From c029675140017773c695f4c9cefff259b8ebd5f5 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Tue, 17 Feb 2026 16:36:16 +0530 Subject: [PATCH 01/20] BT changes for common --- .../main/cpp/native_c4multipeerreplicator.cc | 5 +- .../cpp/native_c4peerdiscoveryprovider.cc | 140 ++++++++++++++---- common/main/cpp/native_glue.cc | 1 + 3 files changed, 115 insertions(+), 31 deletions(-) diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index 9db2bbd95..225859ff7 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -419,8 +419,9 @@ namespace litecore::jni { &authenticateCallback, nullptr, &peerDiscoveredCallback, - &replicatorStatusChangedCallback, - &documentEndedCallback, + reinterpret_cast(&replicatorStatusChangedCallback), + nullptr, + reinterpret_cast(documentEndedCallback), nullptr, nullptr, nullptr, diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 3c34b4219..8410a78b9 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -5,7 +5,6 @@ #include "native_glue.hh" #include "socket_factory.h" #include "MetadataHelper.h" -#include "native_bluetoothpeer_internal.h" #include "com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h" using namespace litecore; @@ -13,6 +12,9 @@ using namespace litecore::jni; using namespace litecore::p2p; namespace litecore::jni { + + + // C4PeerDiscoveryProvider callbacks static jclass cls_C4PeerDiscoveryProvider; static jmethodID m_C4PeerDiscoveryProvider_startBrowsing; @@ -26,7 +28,7 @@ namespace litecore::jni { static jmethodID m_C4PeerDiscoveryProvider_initBleProvider; bool initC4PeerDiscoveryProvider(JNIEnv *env) { - jclass localClass = env->FindClass("com/couchbase/lite/internal/core/BluetoothProvider"); + jclass localClass = env->FindClass("com/couchbase/lite/internal/core/C4PeerDiscoveryProvider"); if (localClass == nullptr) return false; cls_C4PeerDiscoveryProvider = reinterpret_cast(env->NewGlobalRef(localClass)); @@ -131,6 +133,8 @@ namespace litecore::jni { cls_C4PeerDiscoveryProvider, m_C4PeerDiscoveryProvider_startBrowsing, _contextToken); + + if (envState == JNI_EDETACHED) { detachJVM("startBrowsing"); } @@ -193,6 +197,8 @@ namespace litecore::jni { } } + + virtual void monitorMetadata(C4Peer* peer, bool start) override { JNIEnv *env = nullptr; jint envState = attachJVM(&env, "monitorMetadata"); @@ -281,8 +287,8 @@ namespace litecore::jni { } } - fleece::Ref addDiscoveredPeer(C4Peer* peer, bool moreComing = false) { - return addPeer(peer, moreComing); + void addDiscoveredPeer(C4Peer* peer, bool moreComing = false) { + addPeer(peer, moreComing); } void removeDiscoveredPeer(std::string id, bool moreComing = false) { @@ -293,6 +299,10 @@ namespace litecore::jni { statusChanged(m, s); } + bool notifyIncomConnection(C4Peer* peer, C4Socket* s) { + return notifyIncomingConnection(peer, s); + } + void setContextToken(jlong token) { _contextToken = token; } @@ -341,6 +351,23 @@ namespace litecore::p2p { else if (strcmp(uuidStr, litecore::p2p::btle::kPeerGroupUUIDNamespace) == 0) env->SetStaticObjectField(cls, PEER_GROUP_NS_FIELD, uuidObj); } + + bool initBleConstants(JNIEnv* env) { + jUuidClass = env->FindClass("java/util/UUID"); + uuidFromString = env->GetStaticMethodID(jUuidClass, "fromString", + "(Ljava/lang/String;)Ljava/util/UUID;"); + + PORT_CHAR_FIELD = env->GetStaticFieldID(cls_BleP2pConstants, + "PORT_CHARACTERISTIC_ID", "Ljava/util/UUID;"); + META_CHAR_FIELD = env->GetStaticFieldID(cls_BleP2pConstants, + "METADATA_CHARACTERISTIC_ID", "Ljava/util/UUID;"); + + setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kPortCharacteristicID); + setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kMetadataCharacteristicID); + setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kPeerGroupUUIDNamespace); + + return true; + } } @@ -362,19 +389,21 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_service return toJString(env, s); } -JNIEXPORT jlong JNICALL +JNIEXPORT void JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_addPeer( JNIEnv *env, jclass thiz, jlong providerPtr, jstring peerId) { - auto* provider = reinterpret_cast(providerPtr); - if (!provider || !peerId) { return 0; } + // Get the provider instance from the pointer + auto* provider = (C4BLEProvider*)providerPtr; + if (!provider) return; - std::string id = JstringToUTF8(env, peerId); - if (id.empty()) { return 0; } + const char* peerIdStr = env->GetStringUTFChars(peerId, nullptr); + if (!peerIdStr) return; + std::string id(peerIdStr); + env->ReleaseStringUTFChars(peerId, peerIdStr); - auto created = fleece::make_retained(provider, id); - fleece::Ref peer = provider->addDiscoveredPeer(created.get()); - return (jlong) reinterpret_cast(std::move(peer).detach()); + auto* peer = new C4Peer(provider, id); + provider->addDiscoveredPeer(peer); } JNIEXPORT void JNICALL @@ -393,23 +422,45 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_removeP provider->removeDiscoveredPeer(id); } -JNIEXPORT jlong JNICALL +JNIEXPORT jobject JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerWithID( JNIEnv *env, jclass thiz, jlong providerPtr, jstring peerId) { - auto *provider = reinterpret_cast(providerPtr); - if (!provider || !peerId) { return 0; } + // Get provider instance + auto* provider = (C4BLEProvider*)providerPtr; + if (!provider) return nullptr; - std::string peerIdStr = JstringToUTF8(env, peerId); - if (peerIdStr.empty()) { return 0; } + // Convert Java String → C++ string + std::string peerIdStr; + if (peerId) { + const char* peerIdChars = env->GetStringUTFChars(peerId, nullptr); + if (peerIdChars) { + peerIdStr = std::string(peerIdChars); + env->ReleaseStringUTFChars(peerId, peerIdChars); + } + } - C4PeerDiscovery &discovery = provider->discovery(); + // === CALL LIBRARY API === + C4PeerDiscovery& discovery = provider->discovery(); fleece::Retained peer = discovery.peerWithID(peerIdStr); - if (!peer) { return 0; } - C4Peer *rawPeer = std::move(peer).detach(); + // Convert C4Peer* → Java C4Peer object + if (peer) { + // Get the native peer pointer + jlong peerPtr = reinterpret_cast(peer.get()); + + // Create Java BluetoothPeer object + jclass bluetoothPeerClass = env->FindClass("com/couchbase/lite/internal/core/BluetoothPeer"); + if (bluetoothPeerClass == nullptr) return nullptr; - return static_cast(reinterpret_cast(rawPeer)); + jmethodID constructor = env->GetMethodID(bluetoothPeerClass, "", "(J)V"); + if (constructor == nullptr) return nullptr; + + jobject javaPeer = env->NewObject(bluetoothPeerClass, constructor, peerPtr); + + return env->NewGlobalRef(javaPeer); + } + return nullptr; } JNIEXPORT void JNICALL @@ -427,13 +478,8 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerDis // Use the utility function C4Peer::Metadata peerMetadata = javaMapToMetadata(env, metadata); - fleece::Retained peer = provider->discovery().peerWithID(id); - - if (!peer) { - auto created = fleece::make_retained(provider, id); - peer = provider->addDiscoveredPeer(created.get()); - } - peer->setMetadata(std::move(peerMetadata)); + auto* peer = new C4Peer(provider, id, peerMetadata); + provider->addDiscoveredPeer(peer); } JNIEXPORT void JNICALL @@ -454,6 +500,42 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerLos provider->removeDiscoveredPeer(id); } +JNIEXPORT void JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_onIncomingConnection( + JNIEnv *env, jclass thiz, jlong providerPtr, jbyteArray peerId, jlong socketPtr) { + + // Get the provider instance from the pointer + auto provider = (C4BLEProvider*)providerPtr; + if (!provider) return; + + // Convert peerId from Java byte array to C4PeerID + C4PeerID peerIdObj = {}; + if (peerId) { + jbyte* bytes = env->GetByteArrayElements(peerId, nullptr); + if (bytes) { + memcpy(peerIdObj.bytes, bytes, 32); + env->ReleaseByteArrayElements(peerId, bytes, JNI_ABORT); + } + } + + // Get the C4Socket from the pointer + C4Socket* socket = (C4Socket*)socketPtr; + if (!socket) return; + + // Find the peer in the discovery system + auto peer = provider->discovery().peerWithID("peerIdObj"); + if (peer) { + // Notify about the incoming connection + bool accepted = provider->notifyIncomConnection(peer.get(), socket); + + // If not accepted, close the socket + if (!accepted) { + // Close the socket - implementation depends on your socket type + // For BLE, you might close the GATT connection + } + } +} + JNIEXPORT void JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_statusChanged( JNIEnv *env, jclass thiz, jlong providerPtr, jint mode, jboolean online, @@ -501,4 +583,4 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peersWi #ifdef __cplusplus } #endif -#endif // COUCHBASE_ENTERPRISE && __ANDROID__ \ No newline at end of file +#endif // COUCHBASE_ENTERPRISE && __ANDROID__ diff --git a/common/main/cpp/native_glue.cc b/common/main/cpp/native_glue.cc index b3f80756e..d363be73a 100644 --- a/common/main/cpp/native_glue.cc +++ b/common/main/cpp/native_glue.cc @@ -163,6 +163,7 @@ JNI_OnLoad(JavaVM *jvm, void *ignore) { #ifdef __ANDROID__ && initC4MultipeerReplicator(env) && initC4PeerDiscoveryProvider(env) + && initBleConstants(env) #endif #endif && initC4Socket(env)) { From 76792baba63b0c1e6a312f5654e1efdc01c17cc9 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Tue, 17 Feb 2026 23:19:50 +0530 Subject: [PATCH 02/20] Update cmakelist and bluetoothpeer enterprise check --- common/main/cpp/native_bluetoothpeer.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index 5dcccef5d..d873909c5 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -11,6 +11,8 @@ using namespace litecore; using namespace litecore::jni; +#if defined(COUCHBASE_ENTERPRISE) && defined(__ANDROID__) + // Helper to convert C4Peer* from jlong static C4Peer* getPeer(jlong peerPtr) { return reinterpret_cast(peerPtr); @@ -87,4 +89,4 @@ extern "C++" { fleece::release(reinterpret_cast(peerPtr)); } } -#endif \ No newline at end of file +#endif From 25bd5092f701ada2f6d725a4bbf2f6edf8ad8ba0 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Wed, 18 Feb 2026 12:56:54 +0530 Subject: [PATCH 03/20] Updated as build failure due to wrong checks and static declaration --- common/main/cpp/native_bluetoothpeer.cc | 2 -- common/main/cpp/native_c4peerdiscoveryprovider.cc | 3 --- 2 files changed, 5 deletions(-) diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index d873909c5..0a458bc6c 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -11,8 +11,6 @@ using namespace litecore; using namespace litecore::jni; -#if defined(COUCHBASE_ENTERPRISE) && defined(__ANDROID__) - // Helper to convert C4Peer* from jlong static C4Peer* getPeer(jlong peerPtr) { return reinterpret_cast(peerPtr); diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 8410a78b9..cda4be06a 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -12,9 +12,6 @@ using namespace litecore::jni; using namespace litecore::p2p; namespace litecore::jni { - - - // C4PeerDiscoveryProvider callbacks static jclass cls_C4PeerDiscoveryProvider; static jmethodID m_C4PeerDiscoveryProvider_startBrowsing; From 193881ab2d930d05bcd35e41fc16458b21a3445d Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Mon, 23 Feb 2026 11:50:13 +0530 Subject: [PATCH 04/20] Updating multipeer replicator argument to take protocol --- common/main/cpp/native_c4multipeerreplicator.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index 225859ff7..cd894f416 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -419,9 +419,9 @@ namespace litecore::jni { &authenticateCallback, nullptr, &peerDiscoveredCallback, - reinterpret_cast(&replicatorStatusChangedCallback), nullptr, - reinterpret_cast(documentEndedCallback), + &replicatorStatusChangedCallback, + &documentEndedCallback, nullptr, nullptr, nullptr, From ccfcd40abad13c814d77d3439d107ecce2339735 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Fri, 27 Feb 2026 11:45:19 +0530 Subject: [PATCH 05/20] Added release for BT peer and removed onIncommingConnection --- .../cpp/native_c4peerdiscoveryprovider.cc | 54 +++---------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index cda4be06a..fcc7e8bb1 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -25,7 +25,7 @@ namespace litecore::jni { static jmethodID m_C4PeerDiscoveryProvider_initBleProvider; bool initC4PeerDiscoveryProvider(JNIEnv *env) { - jclass localClass = env->FindClass("com/couchbase/lite/internal/core/C4PeerDiscoveryProvider"); + jclass localClass = env->FindClass("com/couchbase/lite/internal/core/BluetoothProvider"); if (localClass == nullptr) return false; cls_C4PeerDiscoveryProvider = reinterpret_cast(env->NewGlobalRef(localClass)); @@ -130,8 +130,6 @@ namespace litecore::jni { cls_C4PeerDiscoveryProvider, m_C4PeerDiscoveryProvider_startBrowsing, _contextToken); - - if (envState == JNI_EDETACHED) { detachJVM("startBrowsing"); } @@ -194,8 +192,6 @@ namespace litecore::jni { } } - - virtual void monitorMetadata(C4Peer* peer, bool start) override { JNIEnv *env = nullptr; jint envState = attachJVM(&env, "monitorMetadata"); @@ -296,10 +292,6 @@ namespace litecore::jni { statusChanged(m, s); } - bool notifyIncomConnection(C4Peer* peer, C4Socket* s) { - return notifyIncomingConnection(peer, s); - } - void setContextToken(jlong token) { _contextToken = token; } @@ -350,6 +342,10 @@ namespace litecore::p2p { } bool initBleConstants(JNIEnv* env) { + jclass localClass = env->FindClass("com/couchbase/lite/internal/p2p/ble/BleP2pConstants"); + if (localClass == nullptr) return false; + + cls_BleP2pConstants = reinterpret_cast(env->NewGlobalRef(localClass)); jUuidClass = env->FindClass("java/util/UUID"); uuidFromString = env->GetStaticMethodID(jUuidClass, "fromString", "(Ljava/lang/String;)Ljava/util/UUID;"); @@ -437,14 +433,14 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerWit } } - // === CALL LIBRARY API === C4PeerDiscovery& discovery = provider->discovery(); fleece::Retained peer = discovery.peerWithID(peerIdStr); // Convert C4Peer* → Java C4Peer object if (peer) { // Get the native peer pointer - jlong peerPtr = reinterpret_cast(peer.get()); + C4Peer* rawPeer = std::move(peer).detach(); + jlong peerPtr = reinterpret_cast(rawPeer); // Create Java BluetoothPeer object jclass bluetoothPeerClass = env->FindClass("com/couchbase/lite/internal/core/BluetoothPeer"); @@ -497,42 +493,6 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerLos provider->removeDiscoveredPeer(id); } -JNIEXPORT void JNICALL -Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_onIncomingConnection( - JNIEnv *env, jclass thiz, jlong providerPtr, jbyteArray peerId, jlong socketPtr) { - - // Get the provider instance from the pointer - auto provider = (C4BLEProvider*)providerPtr; - if (!provider) return; - - // Convert peerId from Java byte array to C4PeerID - C4PeerID peerIdObj = {}; - if (peerId) { - jbyte* bytes = env->GetByteArrayElements(peerId, nullptr); - if (bytes) { - memcpy(peerIdObj.bytes, bytes, 32); - env->ReleaseByteArrayElements(peerId, bytes, JNI_ABORT); - } - } - - // Get the C4Socket from the pointer - C4Socket* socket = (C4Socket*)socketPtr; - if (!socket) return; - - // Find the peer in the discovery system - auto peer = provider->discovery().peerWithID("peerIdObj"); - if (peer) { - // Notify about the incoming connection - bool accepted = provider->notifyIncomConnection(peer.get(), socket); - - // If not accepted, close the socket - if (!accepted) { - // Close the socket - implementation depends on your socket type - // For BLE, you might close the GATT connection - } - } -} - JNIEXPORT void JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_statusChanged( JNIEnv *env, jclass thiz, jlong providerPtr, jint mode, jboolean online, From ed78477e71f0d01d4d9ebab963963c1cd90937e9 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Wed, 4 Mar 2026 01:42:36 +0530 Subject: [PATCH 06/20] Added public transport APIs and removed UUID sync --- ...al_core_impl_NativeC4MultipeerReplicator.h | 2 +- .../main/cpp/native_c4multipeerreplicator.cc | 111 ++++++++++++++++-- .../cpp/native_c4peerdiscoveryprovider.cc | 21 ---- common/main/cpp/native_glue.cc | 1 - 4 files changed, 99 insertions(+), 36 deletions(-) diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h index b7d344e88..2e3725033 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h @@ -20,7 +20,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c jclass, jlong, jstring, - jint, + jobject, jlong, jbyteArray, jlong, diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index cd894f416..14ebedde7 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -101,7 +101,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_createPeerInfo = env->GetStaticMethodID( cls_C4MultipeerReplicator, "createPeerInfo", - "([B[BZ[[BIILcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); + "([B[BZ[[BLjava/util/Set;Lcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); if (m_C4MultipeerReplicator_createPeerInfo == nullptr) return false; @@ -125,7 +125,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onPeerDiscovered = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onPeerDiscovered", - "(J[BIZ)V"); + "(J[BLcom/couchbase/lite/MultipeerTransport;Z)V"); if (m_C4MultipeerReplicator_onPeerDiscovered == nullptr) return false; @@ -133,7 +133,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onReplicatorStatusChanged = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onReplicatorStatusChanged", - "(J[BIZLcom/couchbase/lite/internal/core/C4ReplicatorStatus;)V"); + "(J[BLcom/couchbase/lite/MultipeerTransport;ZLcom/couchbase/lite/internal/core/C4ReplicatorStatus;)V"); if (m_C4MultipeerReplicator_onReplicatorStatusChanged == nullptr) return false; @@ -141,7 +141,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onDocumentEnded = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onDocumentEnded", - "(J[BIZ[Lcom/couchbase/lite/internal/core/C4DocumentEnded;)V"); + "(J[BLcom/couchbase/lite/MultipeerTransport;Z[Lcom/couchbase/lite/internal/core/C4DocumentEnded;)V"); if (m_C4MultipeerReplicator_onDocumentEnded == nullptr) return false; @@ -218,6 +218,89 @@ namespace litecore::jni { return neighborIds; } + static jobject toJavaMultipeerTransport(JNIEnv* env, C4PeerSyncProtocol p) { + jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); + if (!clsTransport) { return nullptr; } + + const char* fieldName; + switch (p) { + case kPeerSyncProtocol_DNS_SD: + fieldName = "WIFI"; + break; + case kPeerSyncProtocol_BluetoothLE: + fieldName = "BLUETOOTH"; + break; + default: + return nullptr; // or choose a default (e.g., WIFI) if your API requires non-null + } + + jfieldID fid = env->GetStaticFieldID( + clsTransport, + fieldName, + "Lcom/couchbase/lite/MultipeerTransport;"); + if (!fid) { return nullptr; } + + return env->GetStaticObjectField(clsTransport, fid); + } + + static jobject toJavaTransportSet(JNIEnv* env, C4PeerSyncProtocols protos) { + jclass clsEnumSet = env->FindClass("java/util/EnumSet"); + jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); + if (!clsEnumSet || !clsTransport) { return nullptr; } + + jmethodID midNoneOf = env->GetStaticMethodID( + clsEnumSet, "noneOf", "(Ljava/lang/Class;)Ljava/util/EnumSet;"); + if (!midNoneOf) { return nullptr; } + + jobject setObj = env->CallStaticObjectMethod(clsEnumSet, midNoneOf, clsTransport); + if (!setObj) { return nullptr; } + + jmethodID midAdd = env->GetMethodID(clsEnumSet, "add", "(Ljava/lang/Object;)Z"); + if (!midAdd) { return setObj; } // empty set (best effort) + + auto addIfPresent = [&](C4PeerSyncProtocol proto) { + if (!(protos & proto)) { return; } + jobject jTransport = toJavaMultipeerTransport(env, proto); + if (!jTransport) { return; } + env->CallBooleanMethod(setObj, midAdd, jTransport); + env->DeleteLocalRef(jTransport); + }; + + addIfPresent(kPeerSyncProtocol_DNS_SD); + addIfPresent(kPeerSyncProtocol_BluetoothLE); + + return setObj; + } + + static C4PeerSyncProtocols toC4PeerSyncProtocols(JNIEnv* env, jobject enumSetTransports) { + if (!enumSetTransports) { return 0; } + + jclass clsSet = env->FindClass("java/util/Set"); + jmethodID midContains = env->GetMethodID(clsSet, "contains", "(Ljava/lang/Object;)Z"); + + jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); + jfieldID fidWifi = env->GetStaticFieldID( + clsTransport, "WIFI", "Lcom/couchbase/lite/MultipeerTransport;"); + jfieldID fidBt = env->GetStaticFieldID( + clsTransport, "BLUETOOTH", "Lcom/couchbase/lite/MultipeerTransport;"); + + jobject jWifi = env->GetStaticObjectField(clsTransport, fidWifi); + jobject jBt = env->GetStaticObjectField(clsTransport, fidBt); + + C4PeerSyncProtocols protos = 0; + + if (env->CallBooleanMethod(enumSetTransports, midContains, jWifi)) { + protos |= kPeerSyncProtocol_DNS_SD; + } + if (env->CallBooleanMethod(enumSetTransports, midContains, jBt)) { + protos |= kPeerSyncProtocol_BluetoothLE; + } + + env->DeleteLocalRef(jWifi); + env->DeleteLocalRef(jBt); + return protos; + } + // The comment over in native_c4replicator.cc applies here as well. // I'm even sorrier that I have to duplicate this mess. static int fromJavaReplColls( @@ -332,12 +415,13 @@ namespace litecore::jni { return; jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); + jobject transport = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onPeerDiscovered, (jlong) context, _peerId, - (jint) protocol, + transport, online ? JNI_TRUE : JNI_FALSE); if (envState == JNI_EDETACHED) { @@ -361,12 +445,13 @@ namespace litecore::jni { jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); jobject _status = toJavaReplStatus(env, *status); + jobject _protocol = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onReplicatorStatusChanged, (jlong) context, _peerId, - (jint) protocol, + _protocol, outbound ? JNI_TRUE : JNI_FALSE, _status); @@ -396,12 +481,13 @@ namespace litecore::jni { jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); jobjectArray _docs = toJavaDocumentEndedArray(env, nDocs, documentEnded); + jobject _protocol = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onDocumentEnded, (jlong) context, _peerId, - (jint) protocol, + _protocol, pushing ? JNI_TRUE : JNI_FALSE, _docs); @@ -418,8 +504,7 @@ namespace litecore::jni { &statusChangedCallback, &authenticateCallback, nullptr, - &peerDiscoveredCallback, - nullptr, + peerDiscoveredCallback, &replicatorStatusChangedCallback, &documentEndedCallback, nullptr, @@ -519,7 +604,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c jclass ignore, jlong token, jstring jgroupId, - jint protocols, + jobject transports, jlong keyPair, jbyteArray cert, jlong c4db, @@ -534,7 +619,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c params.peerGroupID = groupId; // Protocols: - params.protocols = (C4PeerSyncProtocols) protocols; + params.protocols = toC4PeerSyncProtocols(env, transports); // Identity: bool failed; @@ -689,6 +774,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_g jobject replStatus = toJavaReplStatus(env, info->replicatorStatus); jobjectArray neighborIds = fromC4PeerID(env, info->neighbors, info->neighborCount); + jobject transports = toJavaTransportSet(env, info->onlineProtocols); jobject peerInfo = env->CallStaticObjectMethod( cls_C4MultipeerReplicator, @@ -696,8 +782,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_g jpeerId, certChain, info->onlineProtocols > 0 ? JNI_TRUE : JNI_FALSE, - (int) info->onlineProtocols, - (int) info->replicatorProtocol, + transports, neighborIds, replStatus); diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index fcc7e8bb1..8152a9a17 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -340,27 +340,6 @@ namespace litecore::p2p { else if (strcmp(uuidStr, litecore::p2p::btle::kPeerGroupUUIDNamespace) == 0) env->SetStaticObjectField(cls, PEER_GROUP_NS_FIELD, uuidObj); } - - bool initBleConstants(JNIEnv* env) { - jclass localClass = env->FindClass("com/couchbase/lite/internal/p2p/ble/BleP2pConstants"); - if (localClass == nullptr) return false; - - cls_BleP2pConstants = reinterpret_cast(env->NewGlobalRef(localClass)); - jUuidClass = env->FindClass("java/util/UUID"); - uuidFromString = env->GetStaticMethodID(jUuidClass, "fromString", - "(Ljava/lang/String;)Ljava/util/UUID;"); - - PORT_CHAR_FIELD = env->GetStaticFieldID(cls_BleP2pConstants, - "PORT_CHARACTERISTIC_ID", "Ljava/util/UUID;"); - META_CHAR_FIELD = env->GetStaticFieldID(cls_BleP2pConstants, - "METADATA_CHARACTERISTIC_ID", "Ljava/util/UUID;"); - - setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kPortCharacteristicID); - setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kMetadataCharacteristicID); - setUuidConstant(env, cls_BleP2pConstants, litecore::p2p::btle::kPeerGroupUUIDNamespace); - - return true; - } } diff --git a/common/main/cpp/native_glue.cc b/common/main/cpp/native_glue.cc index d363be73a..b3f80756e 100644 --- a/common/main/cpp/native_glue.cc +++ b/common/main/cpp/native_glue.cc @@ -163,7 +163,6 @@ JNI_OnLoad(JavaVM *jvm, void *ignore) { #ifdef __ANDROID__ && initC4MultipeerReplicator(env) && initC4PeerDiscoveryProvider(env) - && initBleConstants(env) #endif #endif && initC4Socket(env)) { From 1c16c3117aa41e7d160ad07d944d792ad0062ee0 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Wed, 4 Mar 2026 19:32:48 +0530 Subject: [PATCH 07/20] Updated the BluetoothPeer object creation in Java instead of native side --- ...e_internal_core_impl_NativeBluetoothPeer.h | 4 ++ common/main/cpp/native_bluetoothpeer.cc | 14 +++++++ .../cpp/native_c4peerdiscoveryprovider.cc | 40 +++++-------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h index 6d19aabae..2853614a9 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h @@ -68,6 +68,10 @@ JNIEXPORT void JNICALL Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_release( JNIEnv *env, jclass clazz, jlong peerPtr); +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_createC4Peer( + JNIEnv* env, jclass /*clazz*/, jlong providerPtr, jstring jPeerId); + #ifdef __cplusplus } #endif diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index 0a458bc6c..b5b999377 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -86,5 +86,19 @@ extern "C++" { JNIEnv *env, jclass clazz, jlong peerPtr) { fleece::release(reinterpret_cast(peerPtr)); } + + JNIEXPORT jlong JNICALL + Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_createC4Peer( + JNIEnv* env, jclass /*clazz*/, jlong providerPtr, jstring jPeerId) { + + auto* provider = reinterpret_cast(providerPtr); + if (!provider || !jPeerId) { return 0; } + + std::string peerId = JstringToUTF8(env, jPeerId); + if (peerId.empty()) { return 0; } + + auto* peer = new C4Peer(provider, peerId); + return (jlong) reinterpret_cast(peer); + } } #endif diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 8152a9a17..dfa755511 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -394,45 +394,23 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_removeP provider->removeDiscoveredPeer(id); } -JNIEXPORT jobject JNICALL +JNIEXPORT jlong JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerWithID( JNIEnv *env, jclass thiz, jlong providerPtr, jstring peerId) { - // Get provider instance - auto* provider = (C4BLEProvider*)providerPtr; - if (!provider) return nullptr; + auto *provider = reinterpret_cast(providerPtr); + if (!provider || !peerId) { return 0; } - // Convert Java String → C++ string - std::string peerIdStr; - if (peerId) { - const char* peerIdChars = env->GetStringUTFChars(peerId, nullptr); - if (peerIdChars) { - peerIdStr = std::string(peerIdChars); - env->ReleaseStringUTFChars(peerId, peerIdChars); - } - } + std::string peerIdStr = JstringToUTF8(env, peerId); + if (peerIdStr.empty()) { return 0; } - C4PeerDiscovery& discovery = provider->discovery(); + C4PeerDiscovery &discovery = provider->discovery(); fleece::Retained peer = discovery.peerWithID(peerIdStr); + if (!peer) { return 0; } - // Convert C4Peer* → Java C4Peer object - if (peer) { - // Get the native peer pointer - C4Peer* rawPeer = std::move(peer).detach(); - jlong peerPtr = reinterpret_cast(rawPeer); + C4Peer *rawPeer = std::move(peer).detach(); - // Create Java BluetoothPeer object - jclass bluetoothPeerClass = env->FindClass("com/couchbase/lite/internal/core/BluetoothPeer"); - if (bluetoothPeerClass == nullptr) return nullptr; - - jmethodID constructor = env->GetMethodID(bluetoothPeerClass, "", "(J)V"); - if (constructor == nullptr) return nullptr; - - jobject javaPeer = env->NewObject(bluetoothPeerClass, constructor, peerPtr); - - return env->NewGlobalRef(javaPeer); - } - return nullptr; + return static_cast(reinterpret_cast(rawPeer)); } JNIEXPORT void JNICALL From 6e5c4f614d6ab75ae1c35ea86afe84aca4d50bde Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Thu, 5 Mar 2026 20:05:43 +0530 Subject: [PATCH 08/20] Updating addpeer and C4Peer creation and protocol usage --- ...e_internal_core_impl_NativeBluetoothPeer.h | 4 --- ...al_core_impl_NativeC4MultipeerReplicator.h | 2 +- common/main/cpp/native_bluetoothpeer.cc | 14 -------- .../main/cpp/native_c4multipeerreplicator.cc | 33 ++----------------- .../cpp/native_c4peerdiscoveryprovider.cc | 32 ++++++++++-------- 5 files changed, 21 insertions(+), 64 deletions(-) diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h index 2853614a9..6d19aabae 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h @@ -68,10 +68,6 @@ JNIEXPORT void JNICALL Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_release( JNIEnv *env, jclass clazz, jlong peerPtr); -JNIEXPORT jlong JNICALL -Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_createC4Peer( - JNIEnv* env, jclass /*clazz*/, jlong providerPtr, jstring jPeerId); - #ifdef __cplusplus } #endif diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h index 2e3725033..b7d344e88 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h @@ -20,7 +20,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c jclass, jlong, jstring, - jobject, + jint, jlong, jbyteArray, jlong, diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index b5b999377..0a458bc6c 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -86,19 +86,5 @@ extern "C++" { JNIEnv *env, jclass clazz, jlong peerPtr) { fleece::release(reinterpret_cast(peerPtr)); } - - JNIEXPORT jlong JNICALL - Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_createC4Peer( - JNIEnv* env, jclass /*clazz*/, jlong providerPtr, jstring jPeerId) { - - auto* provider = reinterpret_cast(providerPtr); - if (!provider || !jPeerId) { return 0; } - - std::string peerId = JstringToUTF8(env, jPeerId); - if (peerId.empty()) { return 0; } - - auto* peer = new C4Peer(provider, peerId); - return (jlong) reinterpret_cast(peer); - } } #endif diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index 14ebedde7..e8e15f21f 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -272,35 +272,6 @@ namespace litecore::jni { return setObj; } - static C4PeerSyncProtocols toC4PeerSyncProtocols(JNIEnv* env, jobject enumSetTransports) { - if (!enumSetTransports) { return 0; } - - jclass clsSet = env->FindClass("java/util/Set"); - jmethodID midContains = env->GetMethodID(clsSet, "contains", "(Ljava/lang/Object;)Z"); - - jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); - jfieldID fidWifi = env->GetStaticFieldID( - clsTransport, "WIFI", "Lcom/couchbase/lite/MultipeerTransport;"); - jfieldID fidBt = env->GetStaticFieldID( - clsTransport, "BLUETOOTH", "Lcom/couchbase/lite/MultipeerTransport;"); - - jobject jWifi = env->GetStaticObjectField(clsTransport, fidWifi); - jobject jBt = env->GetStaticObjectField(clsTransport, fidBt); - - C4PeerSyncProtocols protos = 0; - - if (env->CallBooleanMethod(enumSetTransports, midContains, jWifi)) { - protos |= kPeerSyncProtocol_DNS_SD; - } - if (env->CallBooleanMethod(enumSetTransports, midContains, jBt)) { - protos |= kPeerSyncProtocol_BluetoothLE; - } - - env->DeleteLocalRef(jWifi); - env->DeleteLocalRef(jBt); - return protos; - } - // The comment over in native_c4replicator.cc applies here as well. // I'm even sorrier that I have to duplicate this mess. static int fromJavaReplColls( @@ -604,7 +575,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c jclass ignore, jlong token, jstring jgroupId, - jobject transports, + jint protocols, jlong keyPair, jbyteArray cert, jlong c4db, @@ -619,7 +590,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c params.peerGroupID = groupId; // Protocols: - params.protocols = toC4PeerSyncProtocols(env, transports); + params.protocols = (C4PeerSyncProtocols) protocols; // Identity: bool failed; diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index dfa755511..579f049f0 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -5,6 +5,7 @@ #include "native_glue.hh" #include "socket_factory.h" #include "MetadataHelper.h" +#include "native_bluetoothpeer_internal.h" #include "com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h" using namespace litecore; @@ -280,8 +281,8 @@ namespace litecore::jni { } } - void addDiscoveredPeer(C4Peer* peer, bool moreComing = false) { - addPeer(peer, moreComing); + fleece::Ref addDiscoveredPeer(C4Peer* peer, bool moreComing = false) { + return addPeer(peer, moreComing); } void removeDiscoveredPeer(std::string id, bool moreComing = false) { @@ -361,21 +362,19 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_service return toJString(env, s); } -JNIEXPORT void JNICALL +JNIEXPORT jlong JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_addPeer( JNIEnv *env, jclass thiz, jlong providerPtr, jstring peerId) { - // Get the provider instance from the pointer - auto* provider = (C4BLEProvider*)providerPtr; - if (!provider) return; + auto* provider = reinterpret_cast(providerPtr); + if (!provider || !peerId) { return 0; } - const char* peerIdStr = env->GetStringUTFChars(peerId, nullptr); - if (!peerIdStr) return; - std::string id(peerIdStr); - env->ReleaseStringUTFChars(peerId, peerIdStr); + std::string id = JstringToUTF8(env, peerId); + if (id.empty()) { return 0; } + auto created = fleece::make_retained(provider, id); + fleece::Ref peer = provider->addDiscoveredPeer(created.get()); - auto* peer = new C4Peer(provider, id); - provider->addDiscoveredPeer(peer); + return (jlong) reinterpret_cast(std::move(peer).detach()); } JNIEXPORT void JNICALL @@ -428,8 +427,13 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peerDis // Use the utility function C4Peer::Metadata peerMetadata = javaMapToMetadata(env, metadata); - auto* peer = new C4Peer(provider, id, peerMetadata); - provider->addDiscoveredPeer(peer); + fleece::Retained peer = provider->discovery().peerWithID(id); + + if (!peer) { + auto created = fleece::make_retained(provider, id); + peer = provider->addDiscoveredPeer(created.get()); + } + peer->setMetadata(std::move(peerMetadata)); } JNIEXPORT void JNICALL From fa3eb2a3202e605935279506e65e694640dab106 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Fri, 6 Mar 2026 15:16:11 +0530 Subject: [PATCH 09/20] Conversion of MultipeerTransport and Enumset in java rather than in JNI part --- .../main/cpp/native_c4multipeerreplicator.cc | 76 +++---------------- 1 file changed, 9 insertions(+), 67 deletions(-) diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index e8e15f21f..283b5b011 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -101,7 +101,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_createPeerInfo = env->GetStaticMethodID( cls_C4MultipeerReplicator, "createPeerInfo", - "([B[BZ[[BLjava/util/Set;Lcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); + "([B[BZ[[BILcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); if (m_C4MultipeerReplicator_createPeerInfo == nullptr) return false; @@ -125,7 +125,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onPeerDiscovered = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onPeerDiscovered", - "(J[BLcom/couchbase/lite/MultipeerTransport;Z)V"); + "(J[BIZ)V"); if (m_C4MultipeerReplicator_onPeerDiscovered == nullptr) return false; @@ -133,7 +133,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onReplicatorStatusChanged = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onReplicatorStatusChanged", - "(J[BLcom/couchbase/lite/MultipeerTransport;ZLcom/couchbase/lite/internal/core/C4ReplicatorStatus;)V"); + "(J[BIZLcom/couchbase/lite/internal/core/C4ReplicatorStatus;)V"); if (m_C4MultipeerReplicator_onReplicatorStatusChanged == nullptr) return false; @@ -141,7 +141,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_onDocumentEnded = env->GetStaticMethodID( cls_C4MultipeerReplicator, "onDocumentEnded", - "(J[BLcom/couchbase/lite/MultipeerTransport;Z[Lcom/couchbase/lite/internal/core/C4DocumentEnded;)V"); + "(J[BIZ[Lcom/couchbase/lite/internal/core/C4DocumentEnded;)V"); if (m_C4MultipeerReplicator_onDocumentEnded == nullptr) return false; @@ -218,60 +218,6 @@ namespace litecore::jni { return neighborIds; } - static jobject toJavaMultipeerTransport(JNIEnv* env, C4PeerSyncProtocol p) { - jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); - if (!clsTransport) { return nullptr; } - - const char* fieldName; - switch (p) { - case kPeerSyncProtocol_DNS_SD: - fieldName = "WIFI"; - break; - case kPeerSyncProtocol_BluetoothLE: - fieldName = "BLUETOOTH"; - break; - default: - return nullptr; // or choose a default (e.g., WIFI) if your API requires non-null - } - - jfieldID fid = env->GetStaticFieldID( - clsTransport, - fieldName, - "Lcom/couchbase/lite/MultipeerTransport;"); - if (!fid) { return nullptr; } - - return env->GetStaticObjectField(clsTransport, fid); - } - - static jobject toJavaTransportSet(JNIEnv* env, C4PeerSyncProtocols protos) { - jclass clsEnumSet = env->FindClass("java/util/EnumSet"); - jclass clsTransport = env->FindClass("com/couchbase/lite/MultipeerTransport"); - if (!clsEnumSet || !clsTransport) { return nullptr; } - - jmethodID midNoneOf = env->GetStaticMethodID( - clsEnumSet, "noneOf", "(Ljava/lang/Class;)Ljava/util/EnumSet;"); - if (!midNoneOf) { return nullptr; } - - jobject setObj = env->CallStaticObjectMethod(clsEnumSet, midNoneOf, clsTransport); - if (!setObj) { return nullptr; } - - jmethodID midAdd = env->GetMethodID(clsEnumSet, "add", "(Ljava/lang/Object;)Z"); - if (!midAdd) { return setObj; } // empty set (best effort) - - auto addIfPresent = [&](C4PeerSyncProtocol proto) { - if (!(protos & proto)) { return; } - jobject jTransport = toJavaMultipeerTransport(env, proto); - if (!jTransport) { return; } - env->CallBooleanMethod(setObj, midAdd, jTransport); - env->DeleteLocalRef(jTransport); - }; - - addIfPresent(kPeerSyncProtocol_DNS_SD); - addIfPresent(kPeerSyncProtocol_BluetoothLE); - - return setObj; - } - // The comment over in native_c4replicator.cc applies here as well. // I'm even sorrier that I have to duplicate this mess. static int fromJavaReplColls( @@ -386,13 +332,12 @@ namespace litecore::jni { return; jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); - jobject transport = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onPeerDiscovered, (jlong) context, _peerId, - transport, + (jint) protocol, online ? JNI_TRUE : JNI_FALSE); if (envState == JNI_EDETACHED) { @@ -416,13 +361,12 @@ namespace litecore::jni { jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); jobject _status = toJavaReplStatus(env, *status); - jobject _protocol = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onReplicatorStatusChanged, (jlong) context, _peerId, - _protocol, + (jint) protocol, outbound ? JNI_TRUE : JNI_FALSE, _status); @@ -452,13 +396,12 @@ namespace litecore::jni { jbyteArray _peerId = toJByteArray(env, peerId->bytes, 32); jobjectArray _docs = toJavaDocumentEndedArray(env, nDocs, documentEnded); - jobject _protocol = toJavaMultipeerTransport(env, protocol); env->CallStaticVoidMethod( cls_C4MultipeerReplicator, m_C4MultipeerReplicator_onDocumentEnded, (jlong) context, _peerId, - _protocol, + (jint) protocol, pushing ? JNI_TRUE : JNI_FALSE, _docs); @@ -475,7 +418,7 @@ namespace litecore::jni { &statusChangedCallback, &authenticateCallback, nullptr, - peerDiscoveredCallback, + &peerDiscoveredCallback, &replicatorStatusChangedCallback, &documentEndedCallback, nullptr, @@ -745,7 +688,6 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_g jobject replStatus = toJavaReplStatus(env, info->replicatorStatus); jobjectArray neighborIds = fromC4PeerID(env, info->neighbors, info->neighborCount); - jobject transports = toJavaTransportSet(env, info->onlineProtocols); jobject peerInfo = env->CallStaticObjectMethod( cls_C4MultipeerReplicator, @@ -753,7 +695,7 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_g jpeerId, certChain, info->onlineProtocols > 0 ? JNI_TRUE : JNI_FALSE, - transports, + info->onlineProtocols, neighborIds, replStatus); From f2d2aa6074215cd1306b5eb9496da2a535035663 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Wed, 11 Mar 2026 14:22:10 +0530 Subject: [PATCH 10/20] Adding replicatorTansport API --- common/main/cpp/native_c4multipeerreplicator.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index 283b5b011..9db2bbd95 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -101,7 +101,7 @@ namespace litecore::jni { m_C4MultipeerReplicator_createPeerInfo = env->GetStaticMethodID( cls_C4MultipeerReplicator, "createPeerInfo", - "([B[BZ[[BILcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); + "([B[BZ[[BIILcom/couchbase/lite/internal/core/C4ReplicatorStatus;)Lcom/couchbase/lite/PeerInfo;"); if (m_C4MultipeerReplicator_createPeerInfo == nullptr) return false; @@ -695,7 +695,8 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_g jpeerId, certChain, info->onlineProtocols > 0 ? JNI_TRUE : JNI_FALSE, - info->onlineProtocols, + (int) info->onlineProtocols, + (int) info->replicatorProtocol, neighborIds, replStatus); From 0d1afb93e11bcfe9283e0143989a9451f4cdbef9 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Wed, 11 Mar 2026 14:41:22 +0530 Subject: [PATCH 11/20] BT Socket Factory creation and connecting with java code --- ...ternal_core_impl_NativeC4BTSocketFactory.h | 21 + common/main/cpp/native_c4btsocketfactory.cc | 296 ++++++++++++++ common/main/cpp/native_glue.hh | 1 + .../lite/internal/core/C4BTSocketFactory.java | 247 ++++++++++++ .../lite/internal/core/LiteCoreBTSocket.java | 364 ++++++++++++++++++ .../core/impl/NativeC4BTSocketFactory.java | 33 ++ 6 files changed, 962 insertions(+) create mode 100644 common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h create mode 100644 common/main/cpp/native_c4btsocketfactory.cc create mode 100644 common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java create mode 100644 common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java create mode 100644 common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h new file mode 100644 index 000000000..0f388f4b7 --- /dev/null +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h @@ -0,0 +1,21 @@ +#ifndef ANDROID_COM_COUCHBASE_LITE_INTERNAL_CORE_IMPL_NATIVEC4BTSOCKETFACTORY_H +#define ANDROID_COM_COUCHBASE_LITE_INTERNAL_CORE_IMPL_NATIVEC4BTSOCKETFACTORY_H +#include + +#ifdef __cplusplus +extern "C++" { +#endif + +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_registerBTSocketFactory( + JNIEnv*, jclass); + +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_fromNative( + JNIEnv*, jclass, jlong, jstring, jstring, jlong, jstring, jint); + +#ifdef __cplusplus +} +#endif + +#endif //ANDROID_COM_COUCHBASE_LITE_INTERNAL_CORE_IMPL_NATIVEC4BTSOCKETFACTORY_H diff --git a/common/main/cpp/native_c4btsocketfactory.cc b/common/main/cpp/native_c4btsocketfactory.cc new file mode 100644 index 000000000..9ae68caa1 --- /dev/null +++ b/common/main/cpp/native_c4btsocketfactory.cc @@ -0,0 +1,296 @@ +#if defined(COUCHBASE_ENTERPRISE) && defined(__ANDROID__) + +#include "c4Socket.h" +#include "c4Socket.hh" +#include "c4SocketTypes.h" +#include "native_glue.hh" +#include "com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h" + +#include + +using namespace litecore; +using namespace litecore::jni; + +// ───────────────────────────────────────────────────────────────────────────── +// Java class / method cache +// ───────────────────────────────────────────────────────────────────────────── + +static jclass cls_C4BTSocketFactory; +static jmethodID m_open; +static jmethodID m_write; +static jmethodID m_completedReceive; +static jmethodID m_close; +static jmethodID m_requestClose; +static jmethodID m_dispose; +static jmethodID m_attached; + +bool litecore::jni::initC4BTSocketFactory(JNIEnv* env) { + jclass localCls = env->FindClass( + "com/couchbase/lite/internal/core/C4BTSocketFactory"); + if (!localCls) return false; + + cls_C4BTSocketFactory = reinterpret_cast(env->NewGlobalRef(localCls)); + env->DeleteLocalRef(localCls); + if (!cls_C4BTSocketFactory) return false; + + // static void open(long c4socketPeer, long factoryToken, String peerID) + m_open = env->GetStaticMethodID(cls_C4BTSocketFactory, + "open", "(JJLjava/lang/String;)V"); + if (!m_open) return false; + + // static void write(long c4socketPeer, byte[] data) + m_write = env->GetStaticMethodID(cls_C4BTSocketFactory, + "write", "(J[B)V"); + if (!m_write) return false; + + // static void completedReceive(long c4socketPeer, long byteCount) + m_completedReceive = env->GetStaticMethodID(cls_C4BTSocketFactory, + "completedReceive", "(JJ)V"); + if (!m_completedReceive) return false; + + // static void close(long c4socketPeer) + m_close = env->GetStaticMethodID(cls_C4BTSocketFactory, + "close", "(J)V"); + if (!m_close) return false; + + // static void requestClose(long c4socketPeer, int status, String message) + m_requestClose = env->GetStaticMethodID(cls_C4BTSocketFactory, + "requestClose", "(JILjava/lang/String;)V"); + if (!m_requestClose) return false; + + // static void dispose(long c4socketPeer) + m_dispose = env->GetStaticMethodID(cls_C4BTSocketFactory, + "dispose", "(J)V"); + if (!m_dispose) return false; + + // static void attached(long c4socketPeer, long btSocketHandle, String peerID) + m_attached = env->GetStaticMethodID(cls_C4BTSocketFactory, + "attached", "(JJLjava/lang/String;)V"); + if (!m_attached) return false; + + jniLog("C4BTSocketFactory: callbacks initialized"); + return true; +} + +struct BTNativeHandle { + jlong btSocketHandle; // key into C4BTSocketFactory.sSocketHandles (Java) + std::string peerID; // CBL device-ID / BT MAC address (null-terminated) +}; + +// ───────────────────────────────────────────────────────────────────────────── +// C4SocketFactory callback implementations +// ───────────────────────────────────────────────────────────────────────────── + +static void btOpen(C4Socket* socket, + const C4Address* addr, + C4Slice options, + void* context) { + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btOpen"); + if (envState != JNI_OK && envState != JNI_EDETACHED) return; + + c4socket_retain(socket); + + // addr->hostname carries the CBL peer-ID / BT MAC address as a C4Slice. + // toJString(env, C4Slice) is the correct helper (same as native_c4socket.cc). + jstring jPeerID = toJString(env, addr->hostname); + + env->CallStaticVoidMethod( + cls_C4BTSocketFactory, m_open, + (jlong) socket, + (jlong) context, // factoryToken = context set at registration + jPeerID); + + if (envState == JNI_EDETACHED) { + detachJVM("btOpen"); + } else { + if (jPeerID) env->DeleteLocalRef(jPeerID); + } +} + +static void btWrite(C4Socket* socket, C4SliceResult allocatedData) { + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btWrite"); + if (envState != JNI_OK && envState != JNI_EDETACHED) { + c4slice_free(allocatedData); + return; + } + + jbyteArray jData = toJByteArray(env, allocatedData); + c4slice_free(allocatedData); + + env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_write, (jlong) socket, jData); + + if (envState == JNI_EDETACHED) { + detachJVM("btWrite"); + } else { + if (jData) env->DeleteLocalRef(jData); + } +} + +static void btCompletedReceive(C4Socket* socket, size_t byteCount) { + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btCompletedReceive"); + if (envState != JNI_OK && envState != JNI_EDETACHED) return; + + env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_completedReceive, + (jlong) socket, (jlong) byteCount); + + if (envState == JNI_EDETACHED) detachJVM("btCompletedReceive"); +} + +static void btClose(C4Socket* socket) { + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btClose"); + if (envState != JNI_OK && envState != JNI_EDETACHED) return; + + env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_close, (jlong) socket); + + if (envState == JNI_EDETACHED) detachJVM("btClose"); +} + +static void btRequestClose(C4Socket* socket, int status, C4String messageSlice) { + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btRequestClose"); + if (envState != JNI_OK && envState != JNI_EDETACHED) return; + + // toJString handles a C4Slice (which C4String aliases). + jstring jMessage = toJString(env, messageSlice); + + env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_requestClose, + (jlong) socket, (jint) status, jMessage); + + if (envState == JNI_EDETACHED) { + detachJVM("btRequestClose"); + } else { + if (jMessage) env->DeleteLocalRef(jMessage); + } +} + +static void btDispose(C4Socket* socket) { + auto* ctx = static_cast(socket->getNativeHandle()); + if (ctx) { + socket->setNativeHandle(nullptr); + delete ctx; + } + + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btDispose"); + if (envState != JNI_OK && envState != JNI_EDETACHED) return; + + env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_dispose, (jlong) socket); + + if (envState == JNI_EDETACHED) detachJVM("btDispose"); +} + +/** + * btAttached – called by LiteCore after c4socket_fromNative creates the + * peripheral (incoming) C4Socket. + * + * Retrieves the BTNativeHandle stored by fromNative via setNativeHandle(), + * forwards {btSocketHandle, peerID} to Java, then frees the handle. + */ +static void btAttached(C4Socket* socket) { + auto* ctx = static_cast(socket->getNativeHandle()); + if (!ctx) return; + + // Clear immediately so btDispose won't double-free. + socket->setNativeHandle(nullptr); + + JNIEnv* env = nullptr; + jint envState = attachJVM(&env, "btAttached"); + if (envState != JNI_OK && envState != JNI_EDETACHED) { + delete ctx; + return; + } + + jstring jPeerID = env->NewStringUTF((ctx->peerID).c_str()); + + env->CallStaticVoidMethod( + cls_C4BTSocketFactory, m_attached, + (jlong) socket, + ctx->btSocketHandle, + jPeerID); + + delete ctx; + + if (envState == JNI_EDETACHED) { + detachJVM("btAttached"); + } else { + if (jPeerID) env->DeleteLocalRef(jPeerID); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// The factory struct +// ───────────────────────────────────────────────────────────────────────────── + +static const C4SocketFactory kBTSocketFactory { + .framing = kC4WebSocketClientFraming, + .context = nullptr, // filled in at registration time + .open = btOpen, + .write = btWrite, + .completedReceive = btCompletedReceive, + .close = btClose, + .requestClose = btRequestClose, + .dispose = btDispose, + .attached = btAttached, +}; + +// ───────────────────────────────────────────────────────────────────────────── +// JNI entry points for NativeC4BTSocketFactory +// ───────────────────────────────────────────────────────────────────────────── + +extern "C++" { + +// NativeC4BTSocketFactory.registerBTSocketFactory() +// Registers the factory with LiteCore and returns a context token (jlong). +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_registerBTSocketFactory( + JNIEnv* /*env*/, jclass /*ignore*/) { + // Heap-allocate a mutable copy so context can be set to itself. + auto* factory = new C4SocketFactory(kBTSocketFactory); + factory->context = factory; // token == pointer to the factory copy + c4socket_registerFactory(*factory); + return (jlong) factory; +} + +// NativeC4BTSocketFactory.fromNative(token, scheme, host, port, path, framing) +// Wraps a native BT handle in a C4Socket for the peripheral (incoming) side. +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_fromNative( + JNIEnv* env, jclass /*ignore*/, + jlong jtoken, + jstring jscheme, + jstring jhost, + jlong jport, + jstring jpath, + jint jframing) { + jstringSlice scheme(env, jscheme); + jstringSlice host(env, jhost); + jstringSlice path(env, jpath); + + C4Address addr = {}; + addr.scheme = scheme; + addr.hostname = host; + addr.port = (uint16_t) jport; + addr.path = path; + + void* context = (void*) jtoken; + C4SocketFactory factory = kBTSocketFactory; + factory.framing = (C4SocketFraming) jframing; + factory.context = context; + + C4Socket* c4socket = c4socket_fromNative(factory, context, &addr); + c4socket_retain(c4socket); + + // Store context so btAttached can retrieve it + C4Slice hostSlice = host; // ← implicit conversion to C4Slice + const char* peerStr = static_cast(hostSlice.buf); + auto* ctx = new BTNativeHandle { jport, std::string(peerStr ? peerStr : "", hostSlice.size) }; + c4socket->setNativeHandle(ctx); + return (jlong) c4socket; +} + +} +#endif // COUCHBASE_ENTERPRISE && __ANDROID__ \ No newline at end of file diff --git a/common/main/cpp/native_glue.hh b/common/main/cpp/native_glue.hh index 20b335b7e..7131d1993 100644 --- a/common/main/cpp/native_glue.hh +++ b/common/main/cpp/native_glue.hh @@ -53,6 +53,7 @@ namespace litecore::jni { bool initC4MultipeerReplicator(JNIEnv *); // Implemented in native_c4multipeerreplicator.cc bool initC4PeerDiscoveryProvider(JNIEnv *);// Implemented in native_c4peerdiscoveryprovider.cc + bool initC4BTSocketFactory(JNIEnv *); // Implemented in native_c4btsocketfactory.cc #endif #endif diff --git a/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java b/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java new file mode 100644 index 000000000..88ba910ea --- /dev/null +++ b/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java @@ -0,0 +1,247 @@ +package com.couchbase.lite.internal.core; + +import android.Manifest; +import android.bluetooth.BluetoothSocket; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import com.couchbase.lite.LogDomain; +import com.couchbase.lite.internal.core.impl.NativeC4BTSocketFactory; +import com.couchbase.lite.internal.core.peers.TaggedWeakPeerBinding; +import com.couchbase.lite.internal.fleece.FLEncoder; +import com.couchbase.lite.internal.logging.Log; +import com.couchbase.lite.internal.p2p.ble.BleService; +import com.couchbase.lite.internal.sockets.CloseStatus; +import com.couchbase.lite.internal.sockets.MessageFraming; + +@SuppressWarnings({"checkstyle:CheckNullabilityAnnotations", "PMD.UnusedPrivateMethod"}) +public final class C4BTSocketFactory { + private static final LogDomain LOG_DOMAIN = LogDomain.NETWORK; + + private C4BTSocketFactory() { + // Do Nothing + } + + // ------------------------------------------------------------------------- + // NativeImpl interface (implemented by NativeC4BTSocketFactory) + // ------------------------------------------------------------------------- + + public interface NativeImpl { + /** Register the BT socket factory with LiteCore. Returns a token for unregistering. */ + long nRegisterFactory(); + /** Create a C4Socket wrapping an already-connected native BT handle (peripheral side). */ + long nFromNative(long token, String scheme, String host, long port, String path, int framing); + } + + // ------------------------------------------------------------------------- + // Token → BleService binding (one per registered factory instance) + // ------------------------------------------------------------------------- + + public static class FactoryBinding extends TaggedWeakPeerBinding { + @Override + protected void preBind(long key, @NonNull T obj) {} + @Override + protected void preGetBinding(long key) {} + } + + private static final FactoryBinding BOUND_SERVICES = new FactoryBinding<>(); + + // ------------------------------------------------------------------------- + // Singleton factory token (returned by nRegisterFactory) + // ------------------------------------------------------------------------- + + private static final NativeImpl NATIVE_IMPL = new NativeC4BTSocketFactory(); + private static volatile long sFactoryToken; + + /** + * Register the BT C4SocketFactory with LiteCore and bind the given BleService + * so it can be retrieved by the native callbacks. + * Call once at startup (idempotent). + * + * @param bleService the BleService to use for outgoing L2CAP connections + * @return the factory token (also stored statically) + */ + public static long register(@NonNull BleService bleService) { + if (sFactoryToken == 0L) { + sFactoryToken = NATIVE_IMPL.nRegisterFactory(); + Log.i(LOG_DOMAIN, "C4BTSocketFactory registered, token=%d", sFactoryToken); + } + BOUND_SERVICES.bind(sFactoryToken, bleService); + return sFactoryToken; + } + + public static void triggerIncoming(@NonNull String peerAddr, long btSocketHandle) { + if (sFactoryToken == 0L) { + Log.w(LOG_DOMAIN, "triggerIncoming: factory not registered yet"); + return; + } + NATIVE_IMPL.nFromNative( + sFactoryToken, "blip+bt", peerAddr, 0, "/", + MessageFraming.getC4Framing(MessageFraming.CLIENT_FRAMING)); + } + + @Nullable + private static BleService getBleService(long token) { + return BOUND_SERVICES.getBinding(token); + } + + // ------------------------------------------------------------------------- + // JNI callbacks from native BTSocketFactory + // These are called by the C++ JNI layer (native_c4btsocketfactory.cpp). + // Method signatures must NOT change. + // ------------------------------------------------------------------------- + + /** + * Factory callback: open. + * Called by LiteCore when the replicator wants to open a BT socket to peerID. + * + * @param c4socketPeer native C4Socket pointer + * @param factoryToken the token passed to nRegisterFactory (= context) + * @param peerID Bluetooth device address of the remote peer + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + static void open(long c4socketPeer, long factoryToken, @NonNull String peerID) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.open peer=%s c4socket=0x%x", peerID, c4socketPeer); + final BleService bleService = getBleService(factoryToken); + if (bleService == null) { + Log.w(LOG_DOMAIN, "BTSocketFactory.open: no BleService for token %d", factoryToken); + return; + } + + // Create the C4Socket Java wrapper + final C4Socket c4socket = C4Socket.BOUND_SOCKETS.getBinding(c4socketPeer); + if (c4socket == null) { + Log.w(LOG_DOMAIN, "BTSocketFactory.open: no C4Socket for 0x%x", c4socketPeer); + return; + } + LiteCoreBTSocket.openOutgoing(c4socket, peerID, bleService); + } + + /** + * Factory callback: write. + * Called by LiteCore when it has data to send over the BT socket. + */ + static void write(long c4socketPeer, @NonNull byte[] data) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.write c4socket=0x%x len=%d", c4socketPeer, data.length); + final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); + if (btSocket != null) { + btSocket.coreWrites(data); + } + } + + /** + * Factory callback: completedReceive. + * Called by LiteCore after it has processed n bytes that we delivered. + */ + static void completedReceive(long c4socketPeer, long byteCount) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.completedReceive c4socket=0x%x n=%d", c4socketPeer, byteCount); + final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); + if (btSocket != null) { + btSocket.coreAcksWrite(byteCount); + } + } + + /** + * Factory callback: close. + * Called by LiteCore (byte-stream framing) to request socket teardown. + */ + static void close(long c4socketPeer) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.close c4socket=0x%x", c4socketPeer); + final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); + if (btSocket != null) { + btSocket.coreClosed(); + } + } + + /** + * Factory callback: requestClose. + * Called by LiteCore (message framing) to send a close message to the peer. + */ + static void requestClose(long c4socketPeer, int status, @Nullable String message) { + Log.d( + LOG_DOMAIN, "^BTSocketFactory.requestClose c4socket=0x%x status=%d msg=%s", + c4socketPeer, + status, + message + ); + final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); + if (btSocket != null) { + btSocket.coreRequestsClose(new CloseStatus(C4Constants.ErrorDomain.WEB_SOCKET, status, message)); + } + } + + /** + * Factory callback: dispose. + * Called by LiteCore when the C4Socket is being freed. Remove our binding. + */ + static void dispose(long c4socketPeer) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.dispose c4socket=0x%x", c4socketPeer); + LiteCoreBTSocket.unbindSocket(c4socketPeer); + } + + /** + * Factory callback: attached. + * Called when a C4Socket is created via c4socket_fromNative (peripheral / incoming side). + * + * @param c4socketPeer native C4Socket pointer + * @param btSocketHandle Java handle of the BluetoothSocket (cast to long by native) + * @param peerID CBL peer device ID string + */ + static void attached(long c4socketPeer, long btSocketHandle, @NonNull String peerID) { + Log.d(LOG_DOMAIN, "^BTSocketFactory.attached c4socket=0x%x peer=%s", c4socketPeer, peerID); + final BluetoothSocket btSocket = SOCKET_HANDLES.remove(btSocketHandle); + if (btSocket == null) { + Log.w(LOG_DOMAIN, "BTSocketFactory.attached: no BluetoothSocket for handle %d", btSocketHandle); + return; + } + final C4Socket c4socket = C4Socket.BOUND_SOCKETS.getBinding(c4socketPeer); + if (c4socket == null) { + Log.w(LOG_DOMAIN, "BTSocketFactory.attached: no C4Socket for peer 0x%x", c4socketPeer); + return; + } + LiteCoreBTSocket.attachIncoming(c4socket, btSocket, peerID); + } + + @Nullable + public static byte[] buildUpgradeHeaders() { + try (FLEncoder enc = FLEncoder.getManagedEncoder()) { + enc.beginDict(3); + enc.writeKey("Connection"); + enc.writeString("Upgrade"); + enc.writeKey("Upgrade"); + enc.writeString("websocket"); + enc.writeKey("Sec-WebSocket-Protocol"); + enc.writeString("BLIP_3+CBMobile_4"); + enc.endDict(); + return enc.finish(); + } catch (Exception e) { + return null; + } + } + + // ------------------------------------------------------------------------- + // BluetoothSocket hand-off table + // When the peripheral (server) side accepts an L2CAP connection, the + // BluetoothSocket is stored here keyed by a handle; the handle is passed + // to the native layer which then calls attached() with it. + // ------------------------------------------------------------------------- + + private static final Map SOCKET_HANDLES = new ConcurrentHashMap<>(); + private static final AtomicLong NEXT_HANDLE = new AtomicLong(1L); + + /** + * Register a BluetoothSocket for the peripheral (incoming) path. + * Returns a long handle to be passed to nAttachIncoming. + */ + public static long registerIncomingSocket(@NonNull BluetoothSocket socket) { + final long handle = NEXT_HANDLE.getAndIncrement(); + SOCKET_HANDLES.put(handle, socket); + return handle; + } +} diff --git a/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java b/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java new file mode 100644 index 000000000..96cbbec49 --- /dev/null +++ b/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java @@ -0,0 +1,364 @@ +package com.couchbase.lite.internal.core; + +import android.Manifest; +import android.bluetooth.BluetoothSocket; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import com.couchbase.lite.LogDomain; +import com.couchbase.lite.internal.core.C4Constants.ErrorDomain; +import com.couchbase.lite.internal.core.C4Constants.NetworkError; +import com.couchbase.lite.internal.logging.Log; +import com.couchbase.lite.internal.p2p.ble.BleService; +import com.couchbase.lite.internal.sockets.CloseStatus; +import com.couchbase.lite.internal.sockets.SocketFromCore; + +@SuppressWarnings({"checkstyle:CheckNullabilityAnnotations", "PMD.TooManyFields"}) +public final class LiteCoreBTSocket implements SocketFromCore { + + private static final LogDomain LOG_DOMAIN = LogDomain.NETWORK; + private static final int READ_BUFFER_SIZE = 64 * 1024; + private static final int MAX_RECEIVED_BYTES_PENDING = 256 * 1024; + + private static final Map BOUND_BT_SOCKETS = new ConcurrentHashMap<>(); + + @Nullable + static LiteCoreBTSocket getBoundSocket(long c4socketPeer) { + return BOUND_BT_SOCKETS.get(c4socketPeer); + } + + static void unbindSocket(long c4socketPeer) { + BOUND_BT_SOCKETS.remove(c4socketPeer); + } + + // ------------------------------------------------------------------------- + // Factory methods + // ------------------------------------------------------------------------- + + /** + * Outgoing (central / browsing) side. + * Called from {@link C4BTSocketFactory#open}. + */ + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + @NonNull + public static LiteCoreBTSocket openOutgoing( + @NonNull C4Socket c4socket, + @NonNull String peerID, + @NonNull BleService bleService) { + final LiteCoreBTSocket socket = new LiteCoreBTSocket(peerID, bleService, c4socket); + c4socket.init(socket); + socket.open(); + return socket; + } + + /** + * Incoming (peripheral / publishing) side. + * Called from {@link C4BTSocketFactory#attached}. + */ + @NonNull + public static LiteCoreBTSocket attachIncoming( + @NonNull C4Socket c4socket, + @NonNull BluetoothSocket btSocket, + @NonNull String peerID) { + final LiteCoreBTSocket socket = new LiteCoreBTSocket(peerID, null, c4socket); + c4socket.init(socket); + socket.setChannel(btSocket); + socket.notifyConnected(); + return socket; + } + + // ------------------------------------------------------------------------- + // Fields + // ------------------------------------------------------------------------- + + @NonNull + private final String peerID; + @Nullable + private final BleService bleService; + @NonNull + private final C4Socket c4socket; + + @NonNull + private final ExecutorService socketQueue; + + @NonNull + private final ExecutorService readExecutor; + + @Nullable + private BluetoothSocket btSocket; + @Nullable + private InputStream inputStream; + @Nullable + private OutputStream outputStream; + + @NonNull + private final Deque pendingWrites = new ArrayDeque<>(); + + private long receivedBytesPending; + private volatile boolean connecting; + private volatile boolean closing; + + // ------------------------------------------------------------------------- + // Constructor + // ------------------------------------------------------------------------- + + private LiteCoreBTSocket(@NonNull String peerID, + @Nullable BleService bleService, + @NonNull C4Socket c4socket) { + this.peerID = peerID; + this.bleService = bleService; + this.c4socket = c4socket; + // Now peerID is guaranteed assigned — safe to use in lambda + this.socketQueue = Executors.newSingleThreadExecutor(r -> { + final Thread t = new Thread(r, "litecore-btsocket-" + peerID); + t.setDaemon(true); + return t; + }); + this.readExecutor = Executors.newSingleThreadExecutor(r -> { + final Thread t = new Thread(r, "litecore-btsocket-reader-" + peerID); + t.setDaemon(true); + return t; + }); + + BOUND_BT_SOCKETS.put(getPeerPtr(c4socket), this); + } + + // Retrieve the native peer pointer stored in C4Socket + private static long getPeerPtr(@NonNull C4Socket socket) { + // C4Peer.getPeer() returns the native pointer + return socket.withPeerOrDefault(0L, ptr -> ptr); + } + + // ------------------------------------------------------------------------- + // Outgoing connection + // ------------------------------------------------------------------------- + + @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) + private void open() { + connecting = true; + Log.d(LOG_DOMAIN, "BTSocket %s: open – initiating L2CAP connect", this); + + final boolean initiated = bleService.openPeerSocket( + peerID, + btSocket -> this.onChannelOpened(btSocket), + (addr, err) -> this.closeWithError( + ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, + err != null ? err.getMessage() : "L2CAP channel closed")); + + if (!initiated) { + Log.w(LOG_DOMAIN, "BTSocket %s: peer not found for L2CAP open", this); + socketQueue.execute(() -> + closeWithError(ErrorDomain.NETWORK, NetworkError.HOST_DOWN, + "Bluetooth peer not found: " + peerID)); + } + } + + /** Called by BleService / CblBleDevice once the BluetoothSocket is connected. */ + public void onChannelOpened(@NonNull BluetoothSocket socket) { + socketQueue.execute(() -> { + if (!connecting) { + try { socket.close(); } catch (IOException ignore) {} + return; + } + connecting = false; + setChannel(socket); + notifyConnected(); + }); + } + + private void setChannel(@NonNull BluetoothSocket socket) { + btSocket = socket; + try { + inputStream = socket.getInputStream(); + outputStream = socket.getOutputStream(); + } catch (IOException e) { + Log.w(LOG_DOMAIN, "BTSocket %s: failed to get streams", this, e); + closeWithError(ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, e.getMessage()); + return; + } + readExecutor.execute(this::readLoop); + Log.d(LOG_DOMAIN, "BTSocket %s: channel ready, read loop started", this); + } + + private void notifyConnected() { + Log.i(LOG_DOMAIN, "BTSocket %s: CONNECTED", this); + synchronized (c4socket.getLock()) { + c4socket.ackOpenToCore(101, null); // HTTP 101 Switching Protocols + } + } + + // ------------------------------------------------------------------------- + // SocketFromCore – LiteCore → socket callbacks + // ------------------------------------------------------------------------- + + /** LiteCore requests open (no-op for BT; already handled in factory open callback). */ + @Override + public void coreRequestsOpen() { + Log.d(LOG_DOMAIN, "^BTSocket %s: coreRequestsOpen (redundant, ignoring)", this); + } + + /** LiteCore wants to send data over the wire. */ + @Override + public void coreWrites(@NonNull byte[] data) { + Log.d(LOG_DOMAIN, "^BTSocket %s: coreWrites %d bytes", this, data.length); + socketQueue.execute(() -> { + pendingWrites.addLast(data); + doWrite(); + }); + } + + /** LiteCore finished processing byteCount bytes we delivered. */ + @Override + public void coreAcksWrite(long byteCount) { + Log.d(LOG_DOMAIN, "^BTSocket %s: coreAcksWrite %d", this, byteCount); + socketQueue.execute(() -> { + final boolean wasThrottled = isReadThrottled(); + receivedBytesPending -= byteCount; + if (wasThrottled && !isReadThrottled()) { + readExecutor.execute(this::readLoop); + } + }); + } + + /** LiteCore requests close (byte-stream framing). */ + @Override + public void coreClosed() { + Log.i(LOG_DOMAIN, "^BTSocket %s: coreClosed", this); + socketQueue.execute(() -> closeWithError(0, 0, null)); + } + + /** LiteCore requests WebSocket-level close (message framing). */ + @Override + public void coreRequestsClose(@NonNull CloseStatus status) { + Log.i(LOG_DOMAIN, "^BTSocket %s: coreRequestsClose %d/%d", this, status.domain, status.code); + socketQueue.execute(() -> closeWithError(status.domain, status.code, status.message)); + } + + private void postToSocketQueue(@NonNull Runnable r) { + if (!socketQueue.isShutdown()) { socketQueue.execute(r); } + } + + @SuppressWarnings("PMD.CloseResource") + private void readLoop() { + final InputStream in = inputStream; + if (in == null) { return; } + final byte[] buf = new byte[READ_BUFFER_SIZE]; + + try { + while (!closing) { + if (isReadThrottled()) { return; } // coreAcksWrite will re-submit + + final int n; + try { n = in.read(buf); } + catch (IOException e) { + if (!closing) { + postToSocketQueue(() -> + closeWithError(ErrorDomain.NETWORK, NetworkError.CONNECTION_RESET, e.getMessage())); + } + return; + } + + if (n < 0) { + postToSocketQueue(() -> closeWithError(0, 0, null)); + return; + } + if (n == 0) { continue; } + + final byte[] frame = new byte[n]; + System.arraycopy(buf, 0, frame, 0, n); + + postToSocketQueue(() -> { + receivedBytesPending += frame.length; + Log.d(LOG_DOMAIN, "BTSocket %s: <<< %d bytes [%d pending]", + this, frame.length, receivedBytesPending); + synchronized (c4socket.getLock()) { + c4socket.writeToCore(frame); + } + }); + } + } catch (Exception e) { + if (!closing) { + postToSocketQueue(() -> + closeWithError(ErrorDomain.NETWORK, NetworkError.CONNECTION_RESET, e.getMessage())); + } + } + } + + @SuppressWarnings("PMD.CloseResource") + private void doWrite() { + final OutputStream out = outputStream; + if (out == null) { return; } + + while (!pendingWrites.isEmpty()) { + final byte[] data = pendingWrites.pollFirst(); + if (data == null) { break; } + try { + out.write(data); + out.flush(); + Log.d(LOG_DOMAIN, "BTSocket %s: >>> %d bytes", this, data.length); + synchronized (c4socket.getLock()) { + c4socket.ackWriteToCore(data.length); + } + } catch (IOException e) { + Log.w(LOG_DOMAIN, "BTSocket %s: write error", this, e); + closeWithError(ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, e.getMessage()); + return; + } + } + } + + @SuppressWarnings("PMD.CloseResource") + private void closeWithError(int domain, int code, @Nullable String message) { + if (closing) { return; } + closing = true; + + if (domain == 0) { Log.i(LOG_DOMAIN, "BTSocket %s: CLOSED", this); } + else { Log.w(LOG_DOMAIN, "BTSocket %s: CLOSED err %d/%d '%s'", this, domain, code, message); } + + disconnect(); + + final CloseStatus status = new CloseStatus(domain, code, message); + synchronized (c4socket.getLock()) { + c4socket.closeCore(status); + } + } + + @SuppressWarnings("PMD.CloseResource") + private void disconnect() { + connecting = false; + final InputStream in = inputStream; + final OutputStream out = outputStream; + final BluetoothSocket s = btSocket; + + inputStream = null; + outputStream = null; + btSocket = null; + + if (in != null) { try { in.close(); } catch (IOException ignore) {} } + if (out != null) { try { out.close(); } catch (IOException ignore) {} } + if (s != null) { try { s.close(); } catch (IOException ignore) {} } + + readExecutor.shutdownNow(); + socketQueue.shutdown(); + } + + private boolean isReadThrottled() { + return receivedBytesPending >= MAX_RECEIVED_BYTES_PENDING; + } + + @Override + @NonNull + public String toString() { return "BTSocket[" + peerID + "]"; } +} diff --git a/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java b/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java new file mode 100644 index 000000000..8d19ee137 --- /dev/null +++ b/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java @@ -0,0 +1,33 @@ +package com.couchbase.lite.internal.core.impl; + +import com.couchbase.lite.internal.core.C4BTSocketFactory; + +/** + * JNI implementation of {@link C4BTSocketFactory.NativeImpl}. + */ +public final class NativeC4BTSocketFactory implements C4BTSocketFactory.NativeImpl { + + // ------------------------------------------------------------------------- + // C4BTSocketFactory.NativeImpl + // ------------------------------------------------------------------------- + + @Override + public long nRegisterFactory() { + return registerBTSocketFactory(); + } + + @Override + public long nFromNative(long token, String scheme, String host, long port, String path, int framing) { + return fromNative(token, scheme, host, port, path, framing); + } + + // ------------------------------------------------------------------------- + // Native declarations + // ------------------------------------------------------------------------- + + /** Registers the BTSocketFactory with LiteCore and returns a context token. */ + private static native long registerBTSocketFactory(); + + /** Wrap an existing Java C4BTSocket in a C-native C4Socket (peripheral side). */ + private static native long fromNative(long token, String scheme, String host, long port, String path, int framing); +} From 4c1a7dd853d96a8f38071ae669fdf24183566873 Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Sun, 15 Mar 2026 00:07:40 +0530 Subject: [PATCH 12/20] Socketfactory register and fromnative removal and location update --- common/CMakeLists.txt | 1 + ..._core_impl_NativeC4PeerDiscoveryProvider.h | 13 + .../main/cpp/native_bluetoothpeer_internal.h | 12 + common/main/cpp/native_c4btsocketfactory.cc | 111 +----- .../cpp/native_c4peerdiscoveryprovider.cc | 86 ++++- .../lite/internal/core/C4BTSocketFactory.java | 247 ------------ .../lite/internal/core/C4Socket.java | 14 + .../lite/internal/core/LiteCoreBTSocket.java | 364 ------------------ .../core/impl/NativeC4BTSocketFactory.java | 33 -- 9 files changed, 136 insertions(+), 745 deletions(-) delete mode 100644 common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java delete mode 100644 common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java delete mode 100644 common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 81036c645..6c920ed77 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -68,6 +68,7 @@ add_library( LiteCoreJNI SHARED ${JNI_SRC}/native_c4listener.cc ${JNI_SRC}/native_c4multipeerreplicator.cc ${JNI_SRC}/native_c4peerdiscoveryprovider.cc + ${JNI_SRC}/native_c4btsocketfactory.cc ${JNI_SRC}/native_bluetoothpeer.cc ${JNI_SRC}/MetadataHelper.cc ${JNI_SRC}/native_c4observer.cc diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h index 187c053e5..526fa25f1 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h @@ -54,6 +54,19 @@ JNIEXPORT jstring JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_serviceUuidFromPeerGroup( JNIEnv* env, jclass, jstring peerGroup); +JNIEXPORT void JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_createIncomingSocket( + JNIEnv* env, jclass, + jlong providerPtr, + jlong peerPtr, + jlong btSocketHandle, + jstring jUrl); + +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_getSocketFactory( + JNIEnv* env, jclass, + jlong providerPtr); + #ifdef __cplusplus } #endif diff --git a/common/main/cpp/native_bluetoothpeer_internal.h b/common/main/cpp/native_bluetoothpeer_internal.h index 9fd6b7254..0440a8d3d 100644 --- a/common/main/cpp/native_bluetoothpeer_internal.h +++ b/common/main/cpp/native_bluetoothpeer_internal.h @@ -5,6 +5,18 @@ #include "c4PeerDiscovery.hh" #include "c4Error.h" +#include "c4Socket.h" + +namespace litecore::jni { + /// The BT C4SocketFactory — defined in native_c4btsocketfactory.cc + extern const C4SocketFactory kBTSocketFactory; +} + +struct BTNativeHandle { + jlong btSocketHandle; // key into Java's sSocketHandles map + std::string peerID; // BT MAC address / CBL peer ID +}; + class BluetoothPeer final : public C4Peer { using C4Peer::C4Peer; diff --git a/common/main/cpp/native_c4btsocketfactory.cc b/common/main/cpp/native_c4btsocketfactory.cc index 9ae68caa1..295b6e0d8 100644 --- a/common/main/cpp/native_c4btsocketfactory.cc +++ b/common/main/cpp/native_c4btsocketfactory.cc @@ -4,6 +4,7 @@ #include "c4Socket.hh" #include "c4SocketTypes.h" #include "native_glue.hh" +#include "native_bluetoothpeer_internal.h" #include "com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h" #include @@ -20,13 +21,12 @@ static jmethodID m_open; static jmethodID m_write; static jmethodID m_completedReceive; static jmethodID m_close; -static jmethodID m_requestClose; static jmethodID m_dispose; static jmethodID m_attached; bool litecore::jni::initC4BTSocketFactory(JNIEnv* env) { jclass localCls = env->FindClass( - "com/couchbase/lite/internal/core/C4BTSocketFactory"); + "com/couchbase/lite/internal/BluetoothSocketFactory"); if (!localCls) return false; cls_C4BTSocketFactory = reinterpret_cast(env->NewGlobalRef(localCls)); @@ -53,11 +53,6 @@ bool litecore::jni::initC4BTSocketFactory(JNIEnv* env) { "close", "(J)V"); if (!m_close) return false; - // static void requestClose(long c4socketPeer, int status, String message) - m_requestClose = env->GetStaticMethodID(cls_C4BTSocketFactory, - "requestClose", "(JILjava/lang/String;)V"); - if (!m_requestClose) return false; - // static void dispose(long c4socketPeer) m_dispose = env->GetStaticMethodID(cls_C4BTSocketFactory, "dispose", "(J)V"); @@ -72,11 +67,6 @@ bool litecore::jni::initC4BTSocketFactory(JNIEnv* env) { return true; } -struct BTNativeHandle { - jlong btSocketHandle; // key into C4BTSocketFactory.sSocketHandles (Java) - std::string peerID; // CBL device-ID / BT MAC address (null-terminated) -}; - // ───────────────────────────────────────────────────────────────────────────── // C4SocketFactory callback implementations // ───────────────────────────────────────────────────────────────────────────── @@ -149,24 +139,6 @@ static void btClose(C4Socket* socket) { if (envState == JNI_EDETACHED) detachJVM("btClose"); } -static void btRequestClose(C4Socket* socket, int status, C4String messageSlice) { - JNIEnv* env = nullptr; - jint envState = attachJVM(&env, "btRequestClose"); - if (envState != JNI_OK && envState != JNI_EDETACHED) return; - - // toJString handles a C4Slice (which C4String aliases). - jstring jMessage = toJString(env, messageSlice); - - env->CallStaticVoidMethod(cls_C4BTSocketFactory, m_requestClose, - (jlong) socket, (jint) status, jMessage); - - if (envState == JNI_EDETACHED) { - detachJVM("btRequestClose"); - } else { - if (jMessage) env->DeleteLocalRef(jMessage); - } -} - static void btDispose(C4Socket* socket) { auto* ctx = static_cast(socket->getNativeHandle()); if (ctx) { @@ -224,73 +196,16 @@ static void btAttached(C4Socket* socket) { // ───────────────────────────────────────────────────────────────────────────── // The factory struct // ───────────────────────────────────────────────────────────────────────────── - -static const C4SocketFactory kBTSocketFactory { - .framing = kC4WebSocketClientFraming, - .context = nullptr, // filled in at registration time - .open = btOpen, - .write = btWrite, - .completedReceive = btCompletedReceive, - .close = btClose, - .requestClose = btRequestClose, - .dispose = btDispose, - .attached = btAttached, -}; - -// ───────────────────────────────────────────────────────────────────────────── -// JNI entry points for NativeC4BTSocketFactory -// ───────────────────────────────────────────────────────────────────────────── - -extern "C++" { - -// NativeC4BTSocketFactory.registerBTSocketFactory() -// Registers the factory with LiteCore and returns a context token (jlong). -JNIEXPORT jlong JNICALL -Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_registerBTSocketFactory( - JNIEnv* /*env*/, jclass /*ignore*/) { - // Heap-allocate a mutable copy so context can be set to itself. - auto* factory = new C4SocketFactory(kBTSocketFactory); - factory->context = factory; // token == pointer to the factory copy - c4socket_registerFactory(*factory); - return (jlong) factory; -} - -// NativeC4BTSocketFactory.fromNative(token, scheme, host, port, path, framing) -// Wraps a native BT handle in a C4Socket for the peripheral (incoming) side. -JNIEXPORT jlong JNICALL -Java_com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory_fromNative( - JNIEnv* env, jclass /*ignore*/, - jlong jtoken, - jstring jscheme, - jstring jhost, - jlong jport, - jstring jpath, - jint jframing) { - jstringSlice scheme(env, jscheme); - jstringSlice host(env, jhost); - jstringSlice path(env, jpath); - - C4Address addr = {}; - addr.scheme = scheme; - addr.hostname = host; - addr.port = (uint16_t) jport; - addr.path = path; - - void* context = (void*) jtoken; - C4SocketFactory factory = kBTSocketFactory; - factory.framing = (C4SocketFraming) jframing; - factory.context = context; - - C4Socket* c4socket = c4socket_fromNative(factory, context, &addr); - c4socket_retain(c4socket); - - // Store context so btAttached can retrieve it - C4Slice hostSlice = host; // ← implicit conversion to C4Slice - const char* peerStr = static_cast(hostSlice.buf); - auto* ctx = new BTNativeHandle { jport, std::string(peerStr ? peerStr : "", hostSlice.size) }; - c4socket->setNativeHandle(ctx); - return (jlong) c4socket; -} - +namespace litecore::jni { + const C4SocketFactory kBTSocketFactory{ + .framing = kC4WebSocketClientFraming, + .context = nullptr, // filled in at registration time + .open = btOpen, + .write = btWrite, + .completedReceive = btCompletedReceive, + .close = btClose, + .dispose = btDispose, + .attached = btAttached, + }; } #endif // COUCHBASE_ENTERPRISE && __ANDROID__ \ No newline at end of file diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 579f049f0..1698ce720 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -5,7 +5,10 @@ #include "native_glue.hh" #include "socket_factory.h" #include "MetadataHelper.h" +#include "TLSCodec.hh" +#include "c4Socket.hh" #include "native_bluetoothpeer_internal.h" +#include "com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h" #include "com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h" using namespace litecore; @@ -88,13 +91,11 @@ namespace litecore::jni { } - class C4BLEProvider : public C4PeerDiscoveryProvider { public: C4BLEProvider(C4PeerDiscovery& discovery, std::string_view peerGroupID) : C4PeerDiscoveryProvider(discovery, kPeerSyncProtocol_BluetoothLE, peerGroupID) { std::string pg(peerGroupID); - JNIEnv* env = nullptr; jint envState = attachJVM(&env, "initBleProvider"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; @@ -113,8 +114,9 @@ namespace litecore::jni { env->ExceptionClear(); token = 0; } - _contextToken = token; + _socket = litecore::jni::kBTSocketFactory; + _socket.context = this; if (envState == JNI_EDETACHED) detachJVM("initBleProvider"); if (jPeerGroup) env->DeleteLocalRef(jPeerGroup); @@ -297,8 +299,21 @@ namespace litecore::jni { _contextToken = token; } + std::optional getSocketFactory() const override { + return net::wrapSocketFactoryInTLS(_socket); + } + + bool notifyIncomingConn(C4Peer* peer, C4Socket* socket) { + return notifyIncomingConnection(peer, socket); + } + + fleece::Ref createIncomingSockWithTLS(C4SocketFactory& factory, void* ctx, C4Address address) { + return createIncomingSocketWithTLS(factory, ctx, address); + } + private: jlong _contextToken{}; + C4SocketFactory _socket; }; // Factory function for creating BLE provider @@ -498,6 +513,71 @@ Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peersWi return arr; } +JNIEXPORT void JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_createIncomingSocket( + JNIEnv* env, jclass, + jlong providerPtr, + jlong peerPtr, + jlong btSocketHandle, + jstring jUrl) +{ + auto* provider = reinterpret_cast(providerPtr); + auto* peer = reinterpret_cast(peerPtr); + if (!provider || !peer) return; + + // Parse URL → C4Address + jstringSlice urlSlice(env, jUrl); + C4Address address{}; + if (!c4address_fromURL(urlSlice, &address, nullptr)) { + C4Warn("nCreateIncomingSocket: failed to parse URL"); + return; + } + + auto* ctx = new BTNativeHandle { + btSocketHandle, + peer->id + }; + + std::optional factory = provider->getSocketFactory(); + if (!factory.has_value()) { + C4Warn("nCreateIncomingSocket: no socket factory"); + delete ctx; + return; + } + + fleece::Ref socket = provider->createIncomingSockWithTLS( + *factory, + reinterpret_cast(ctx), + address); + + if (!socket) { + C4Warn("nCreateIncomingSocket: createIncomingSocketWithTLS failed"); + delete ctx; + return; + } + + if (!provider->notifyIncomingConn(peer, socket)) { + socket->getFactory().close(socket); + } +} + +JNIEXPORT jlong JNICALL +Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_getSocketFactory( + JNIEnv* env, jclass, + jlong providerPtr) +{ + auto* provider = reinterpret_cast(providerPtr); + if (!provider) return 0L; + + std::optional factory = provider->getSocketFactory(); + + if (!factory.has_value()) return 0L; + + auto* factoryPtr = new C4SocketFactory(factory.value()); + return reinterpret_cast(factoryPtr); +} + + #ifdef __cplusplus } #endif diff --git a/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java b/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java deleted file mode 100644 index 88ba910ea..000000000 --- a/common/main/java/com/couchbase/lite/internal/core/C4BTSocketFactory.java +++ /dev/null @@ -1,247 +0,0 @@ -package com.couchbase.lite.internal.core; - -import android.Manifest; -import android.bluetooth.BluetoothSocket; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresPermission; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -import com.couchbase.lite.LogDomain; -import com.couchbase.lite.internal.core.impl.NativeC4BTSocketFactory; -import com.couchbase.lite.internal.core.peers.TaggedWeakPeerBinding; -import com.couchbase.lite.internal.fleece.FLEncoder; -import com.couchbase.lite.internal.logging.Log; -import com.couchbase.lite.internal.p2p.ble.BleService; -import com.couchbase.lite.internal.sockets.CloseStatus; -import com.couchbase.lite.internal.sockets.MessageFraming; - -@SuppressWarnings({"checkstyle:CheckNullabilityAnnotations", "PMD.UnusedPrivateMethod"}) -public final class C4BTSocketFactory { - private static final LogDomain LOG_DOMAIN = LogDomain.NETWORK; - - private C4BTSocketFactory() { - // Do Nothing - } - - // ------------------------------------------------------------------------- - // NativeImpl interface (implemented by NativeC4BTSocketFactory) - // ------------------------------------------------------------------------- - - public interface NativeImpl { - /** Register the BT socket factory with LiteCore. Returns a token for unregistering. */ - long nRegisterFactory(); - /** Create a C4Socket wrapping an already-connected native BT handle (peripheral side). */ - long nFromNative(long token, String scheme, String host, long port, String path, int framing); - } - - // ------------------------------------------------------------------------- - // Token → BleService binding (one per registered factory instance) - // ------------------------------------------------------------------------- - - public static class FactoryBinding extends TaggedWeakPeerBinding { - @Override - protected void preBind(long key, @NonNull T obj) {} - @Override - protected void preGetBinding(long key) {} - } - - private static final FactoryBinding BOUND_SERVICES = new FactoryBinding<>(); - - // ------------------------------------------------------------------------- - // Singleton factory token (returned by nRegisterFactory) - // ------------------------------------------------------------------------- - - private static final NativeImpl NATIVE_IMPL = new NativeC4BTSocketFactory(); - private static volatile long sFactoryToken; - - /** - * Register the BT C4SocketFactory with LiteCore and bind the given BleService - * so it can be retrieved by the native callbacks. - * Call once at startup (idempotent). - * - * @param bleService the BleService to use for outgoing L2CAP connections - * @return the factory token (also stored statically) - */ - public static long register(@NonNull BleService bleService) { - if (sFactoryToken == 0L) { - sFactoryToken = NATIVE_IMPL.nRegisterFactory(); - Log.i(LOG_DOMAIN, "C4BTSocketFactory registered, token=%d", sFactoryToken); - } - BOUND_SERVICES.bind(sFactoryToken, bleService); - return sFactoryToken; - } - - public static void triggerIncoming(@NonNull String peerAddr, long btSocketHandle) { - if (sFactoryToken == 0L) { - Log.w(LOG_DOMAIN, "triggerIncoming: factory not registered yet"); - return; - } - NATIVE_IMPL.nFromNative( - sFactoryToken, "blip+bt", peerAddr, 0, "/", - MessageFraming.getC4Framing(MessageFraming.CLIENT_FRAMING)); - } - - @Nullable - private static BleService getBleService(long token) { - return BOUND_SERVICES.getBinding(token); - } - - // ------------------------------------------------------------------------- - // JNI callbacks from native BTSocketFactory - // These are called by the C++ JNI layer (native_c4btsocketfactory.cpp). - // Method signatures must NOT change. - // ------------------------------------------------------------------------- - - /** - * Factory callback: open. - * Called by LiteCore when the replicator wants to open a BT socket to peerID. - * - * @param c4socketPeer native C4Socket pointer - * @param factoryToken the token passed to nRegisterFactory (= context) - * @param peerID Bluetooth device address of the remote peer - */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - static void open(long c4socketPeer, long factoryToken, @NonNull String peerID) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.open peer=%s c4socket=0x%x", peerID, c4socketPeer); - final BleService bleService = getBleService(factoryToken); - if (bleService == null) { - Log.w(LOG_DOMAIN, "BTSocketFactory.open: no BleService for token %d", factoryToken); - return; - } - - // Create the C4Socket Java wrapper - final C4Socket c4socket = C4Socket.BOUND_SOCKETS.getBinding(c4socketPeer); - if (c4socket == null) { - Log.w(LOG_DOMAIN, "BTSocketFactory.open: no C4Socket for 0x%x", c4socketPeer); - return; - } - LiteCoreBTSocket.openOutgoing(c4socket, peerID, bleService); - } - - /** - * Factory callback: write. - * Called by LiteCore when it has data to send over the BT socket. - */ - static void write(long c4socketPeer, @NonNull byte[] data) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.write c4socket=0x%x len=%d", c4socketPeer, data.length); - final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); - if (btSocket != null) { - btSocket.coreWrites(data); - } - } - - /** - * Factory callback: completedReceive. - * Called by LiteCore after it has processed n bytes that we delivered. - */ - static void completedReceive(long c4socketPeer, long byteCount) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.completedReceive c4socket=0x%x n=%d", c4socketPeer, byteCount); - final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); - if (btSocket != null) { - btSocket.coreAcksWrite(byteCount); - } - } - - /** - * Factory callback: close. - * Called by LiteCore (byte-stream framing) to request socket teardown. - */ - static void close(long c4socketPeer) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.close c4socket=0x%x", c4socketPeer); - final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); - if (btSocket != null) { - btSocket.coreClosed(); - } - } - - /** - * Factory callback: requestClose. - * Called by LiteCore (message framing) to send a close message to the peer. - */ - static void requestClose(long c4socketPeer, int status, @Nullable String message) { - Log.d( - LOG_DOMAIN, "^BTSocketFactory.requestClose c4socket=0x%x status=%d msg=%s", - c4socketPeer, - status, - message - ); - final LiteCoreBTSocket btSocket = LiteCoreBTSocket.getBoundSocket(c4socketPeer); - if (btSocket != null) { - btSocket.coreRequestsClose(new CloseStatus(C4Constants.ErrorDomain.WEB_SOCKET, status, message)); - } - } - - /** - * Factory callback: dispose. - * Called by LiteCore when the C4Socket is being freed. Remove our binding. - */ - static void dispose(long c4socketPeer) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.dispose c4socket=0x%x", c4socketPeer); - LiteCoreBTSocket.unbindSocket(c4socketPeer); - } - - /** - * Factory callback: attached. - * Called when a C4Socket is created via c4socket_fromNative (peripheral / incoming side). - * - * @param c4socketPeer native C4Socket pointer - * @param btSocketHandle Java handle of the BluetoothSocket (cast to long by native) - * @param peerID CBL peer device ID string - */ - static void attached(long c4socketPeer, long btSocketHandle, @NonNull String peerID) { - Log.d(LOG_DOMAIN, "^BTSocketFactory.attached c4socket=0x%x peer=%s", c4socketPeer, peerID); - final BluetoothSocket btSocket = SOCKET_HANDLES.remove(btSocketHandle); - if (btSocket == null) { - Log.w(LOG_DOMAIN, "BTSocketFactory.attached: no BluetoothSocket for handle %d", btSocketHandle); - return; - } - final C4Socket c4socket = C4Socket.BOUND_SOCKETS.getBinding(c4socketPeer); - if (c4socket == null) { - Log.w(LOG_DOMAIN, "BTSocketFactory.attached: no C4Socket for peer 0x%x", c4socketPeer); - return; - } - LiteCoreBTSocket.attachIncoming(c4socket, btSocket, peerID); - } - - @Nullable - public static byte[] buildUpgradeHeaders() { - try (FLEncoder enc = FLEncoder.getManagedEncoder()) { - enc.beginDict(3); - enc.writeKey("Connection"); - enc.writeString("Upgrade"); - enc.writeKey("Upgrade"); - enc.writeString("websocket"); - enc.writeKey("Sec-WebSocket-Protocol"); - enc.writeString("BLIP_3+CBMobile_4"); - enc.endDict(); - return enc.finish(); - } catch (Exception e) { - return null; - } - } - - // ------------------------------------------------------------------------- - // BluetoothSocket hand-off table - // When the peripheral (server) side accepts an L2CAP connection, the - // BluetoothSocket is stored here keyed by a handle; the handle is passed - // to the native layer which then calls attached() with it. - // ------------------------------------------------------------------------- - - private static final Map SOCKET_HANDLES = new ConcurrentHashMap<>(); - private static final AtomicLong NEXT_HANDLE = new AtomicLong(1L); - - /** - * Register a BluetoothSocket for the peripheral (incoming) path. - * Returns a long handle to be passed to nAttachIncoming. - */ - public static long registerIncomingSocket(@NonNull BluetoothSocket socket) { - final long handle = NEXT_HANDLE.getAndIncrement(); - SOCKET_HANDLES.put(handle, socket); - return handle; - } -} diff --git a/common/main/java/com/couchbase/lite/internal/core/C4Socket.java b/common/main/java/com/couchbase/lite/internal/core/C4Socket.java index 4b83c1c2a..b20d04cc8 100644 --- a/common/main/java/com/couchbase/lite/internal/core/C4Socket.java +++ b/common/main/java/com/couchbase/lite/internal/core/C4Socket.java @@ -144,6 +144,20 @@ static C4Socket createSocket(@NonNull NativeImpl impl, long peer) { return socket; } + @NonNull + public static C4Socket wrapNativePeer(long peer) { + return createSocket(NATIVE_IMPL, peer); + } + + @Nullable + public static C4Socket getSocket(long peer) { + return BOUND_SOCKETS.getBinding(peer); + } + + public long getPeerPtr() { + return withPeerOrDefault(0L, ptr -> ptr); + } + //------------------------------------------------------------------------- // JNI callback methods //------------------------------------------------------------------------- diff --git a/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java b/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java deleted file mode 100644 index 96cbbec49..000000000 --- a/common/main/java/com/couchbase/lite/internal/core/LiteCoreBTSocket.java +++ /dev/null @@ -1,364 +0,0 @@ -package com.couchbase.lite.internal.core; - -import android.Manifest; -import android.bluetooth.BluetoothSocket; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresPermission; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.couchbase.lite.LogDomain; -import com.couchbase.lite.internal.core.C4Constants.ErrorDomain; -import com.couchbase.lite.internal.core.C4Constants.NetworkError; -import com.couchbase.lite.internal.logging.Log; -import com.couchbase.lite.internal.p2p.ble.BleService; -import com.couchbase.lite.internal.sockets.CloseStatus; -import com.couchbase.lite.internal.sockets.SocketFromCore; - -@SuppressWarnings({"checkstyle:CheckNullabilityAnnotations", "PMD.TooManyFields"}) -public final class LiteCoreBTSocket implements SocketFromCore { - - private static final LogDomain LOG_DOMAIN = LogDomain.NETWORK; - private static final int READ_BUFFER_SIZE = 64 * 1024; - private static final int MAX_RECEIVED_BYTES_PENDING = 256 * 1024; - - private static final Map BOUND_BT_SOCKETS = new ConcurrentHashMap<>(); - - @Nullable - static LiteCoreBTSocket getBoundSocket(long c4socketPeer) { - return BOUND_BT_SOCKETS.get(c4socketPeer); - } - - static void unbindSocket(long c4socketPeer) { - BOUND_BT_SOCKETS.remove(c4socketPeer); - } - - // ------------------------------------------------------------------------- - // Factory methods - // ------------------------------------------------------------------------- - - /** - * Outgoing (central / browsing) side. - * Called from {@link C4BTSocketFactory#open}. - */ - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @NonNull - public static LiteCoreBTSocket openOutgoing( - @NonNull C4Socket c4socket, - @NonNull String peerID, - @NonNull BleService bleService) { - final LiteCoreBTSocket socket = new LiteCoreBTSocket(peerID, bleService, c4socket); - c4socket.init(socket); - socket.open(); - return socket; - } - - /** - * Incoming (peripheral / publishing) side. - * Called from {@link C4BTSocketFactory#attached}. - */ - @NonNull - public static LiteCoreBTSocket attachIncoming( - @NonNull C4Socket c4socket, - @NonNull BluetoothSocket btSocket, - @NonNull String peerID) { - final LiteCoreBTSocket socket = new LiteCoreBTSocket(peerID, null, c4socket); - c4socket.init(socket); - socket.setChannel(btSocket); - socket.notifyConnected(); - return socket; - } - - // ------------------------------------------------------------------------- - // Fields - // ------------------------------------------------------------------------- - - @NonNull - private final String peerID; - @Nullable - private final BleService bleService; - @NonNull - private final C4Socket c4socket; - - @NonNull - private final ExecutorService socketQueue; - - @NonNull - private final ExecutorService readExecutor; - - @Nullable - private BluetoothSocket btSocket; - @Nullable - private InputStream inputStream; - @Nullable - private OutputStream outputStream; - - @NonNull - private final Deque pendingWrites = new ArrayDeque<>(); - - private long receivedBytesPending; - private volatile boolean connecting; - private volatile boolean closing; - - // ------------------------------------------------------------------------- - // Constructor - // ------------------------------------------------------------------------- - - private LiteCoreBTSocket(@NonNull String peerID, - @Nullable BleService bleService, - @NonNull C4Socket c4socket) { - this.peerID = peerID; - this.bleService = bleService; - this.c4socket = c4socket; - // Now peerID is guaranteed assigned — safe to use in lambda - this.socketQueue = Executors.newSingleThreadExecutor(r -> { - final Thread t = new Thread(r, "litecore-btsocket-" + peerID); - t.setDaemon(true); - return t; - }); - this.readExecutor = Executors.newSingleThreadExecutor(r -> { - final Thread t = new Thread(r, "litecore-btsocket-reader-" + peerID); - t.setDaemon(true); - return t; - }); - - BOUND_BT_SOCKETS.put(getPeerPtr(c4socket), this); - } - - // Retrieve the native peer pointer stored in C4Socket - private static long getPeerPtr(@NonNull C4Socket socket) { - // C4Peer.getPeer() returns the native pointer - return socket.withPeerOrDefault(0L, ptr -> ptr); - } - - // ------------------------------------------------------------------------- - // Outgoing connection - // ------------------------------------------------------------------------- - - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - private void open() { - connecting = true; - Log.d(LOG_DOMAIN, "BTSocket %s: open – initiating L2CAP connect", this); - - final boolean initiated = bleService.openPeerSocket( - peerID, - btSocket -> this.onChannelOpened(btSocket), - (addr, err) -> this.closeWithError( - ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, - err != null ? err.getMessage() : "L2CAP channel closed")); - - if (!initiated) { - Log.w(LOG_DOMAIN, "BTSocket %s: peer not found for L2CAP open", this); - socketQueue.execute(() -> - closeWithError(ErrorDomain.NETWORK, NetworkError.HOST_DOWN, - "Bluetooth peer not found: " + peerID)); - } - } - - /** Called by BleService / CblBleDevice once the BluetoothSocket is connected. */ - public void onChannelOpened(@NonNull BluetoothSocket socket) { - socketQueue.execute(() -> { - if (!connecting) { - try { socket.close(); } catch (IOException ignore) {} - return; - } - connecting = false; - setChannel(socket); - notifyConnected(); - }); - } - - private void setChannel(@NonNull BluetoothSocket socket) { - btSocket = socket; - try { - inputStream = socket.getInputStream(); - outputStream = socket.getOutputStream(); - } catch (IOException e) { - Log.w(LOG_DOMAIN, "BTSocket %s: failed to get streams", this, e); - closeWithError(ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, e.getMessage()); - return; - } - readExecutor.execute(this::readLoop); - Log.d(LOG_DOMAIN, "BTSocket %s: channel ready, read loop started", this); - } - - private void notifyConnected() { - Log.i(LOG_DOMAIN, "BTSocket %s: CONNECTED", this); - synchronized (c4socket.getLock()) { - c4socket.ackOpenToCore(101, null); // HTTP 101 Switching Protocols - } - } - - // ------------------------------------------------------------------------- - // SocketFromCore – LiteCore → socket callbacks - // ------------------------------------------------------------------------- - - /** LiteCore requests open (no-op for BT; already handled in factory open callback). */ - @Override - public void coreRequestsOpen() { - Log.d(LOG_DOMAIN, "^BTSocket %s: coreRequestsOpen (redundant, ignoring)", this); - } - - /** LiteCore wants to send data over the wire. */ - @Override - public void coreWrites(@NonNull byte[] data) { - Log.d(LOG_DOMAIN, "^BTSocket %s: coreWrites %d bytes", this, data.length); - socketQueue.execute(() -> { - pendingWrites.addLast(data); - doWrite(); - }); - } - - /** LiteCore finished processing byteCount bytes we delivered. */ - @Override - public void coreAcksWrite(long byteCount) { - Log.d(LOG_DOMAIN, "^BTSocket %s: coreAcksWrite %d", this, byteCount); - socketQueue.execute(() -> { - final boolean wasThrottled = isReadThrottled(); - receivedBytesPending -= byteCount; - if (wasThrottled && !isReadThrottled()) { - readExecutor.execute(this::readLoop); - } - }); - } - - /** LiteCore requests close (byte-stream framing). */ - @Override - public void coreClosed() { - Log.i(LOG_DOMAIN, "^BTSocket %s: coreClosed", this); - socketQueue.execute(() -> closeWithError(0, 0, null)); - } - - /** LiteCore requests WebSocket-level close (message framing). */ - @Override - public void coreRequestsClose(@NonNull CloseStatus status) { - Log.i(LOG_DOMAIN, "^BTSocket %s: coreRequestsClose %d/%d", this, status.domain, status.code); - socketQueue.execute(() -> closeWithError(status.domain, status.code, status.message)); - } - - private void postToSocketQueue(@NonNull Runnable r) { - if (!socketQueue.isShutdown()) { socketQueue.execute(r); } - } - - @SuppressWarnings("PMD.CloseResource") - private void readLoop() { - final InputStream in = inputStream; - if (in == null) { return; } - final byte[] buf = new byte[READ_BUFFER_SIZE]; - - try { - while (!closing) { - if (isReadThrottled()) { return; } // coreAcksWrite will re-submit - - final int n; - try { n = in.read(buf); } - catch (IOException e) { - if (!closing) { - postToSocketQueue(() -> - closeWithError(ErrorDomain.NETWORK, NetworkError.CONNECTION_RESET, e.getMessage())); - } - return; - } - - if (n < 0) { - postToSocketQueue(() -> closeWithError(0, 0, null)); - return; - } - if (n == 0) { continue; } - - final byte[] frame = new byte[n]; - System.arraycopy(buf, 0, frame, 0, n); - - postToSocketQueue(() -> { - receivedBytesPending += frame.length; - Log.d(LOG_DOMAIN, "BTSocket %s: <<< %d bytes [%d pending]", - this, frame.length, receivedBytesPending); - synchronized (c4socket.getLock()) { - c4socket.writeToCore(frame); - } - }); - } - } catch (Exception e) { - if (!closing) { - postToSocketQueue(() -> - closeWithError(ErrorDomain.NETWORK, NetworkError.CONNECTION_RESET, e.getMessage())); - } - } - } - - @SuppressWarnings("PMD.CloseResource") - private void doWrite() { - final OutputStream out = outputStream; - if (out == null) { return; } - - while (!pendingWrites.isEmpty()) { - final byte[] data = pendingWrites.pollFirst(); - if (data == null) { break; } - try { - out.write(data); - out.flush(); - Log.d(LOG_DOMAIN, "BTSocket %s: >>> %d bytes", this, data.length); - synchronized (c4socket.getLock()) { - c4socket.ackWriteToCore(data.length); - } - } catch (IOException e) { - Log.w(LOG_DOMAIN, "BTSocket %s: write error", this, e); - closeWithError(ErrorDomain.NETWORK, NetworkError.BROKEN_PIPE, e.getMessage()); - return; - } - } - } - - @SuppressWarnings("PMD.CloseResource") - private void closeWithError(int domain, int code, @Nullable String message) { - if (closing) { return; } - closing = true; - - if (domain == 0) { Log.i(LOG_DOMAIN, "BTSocket %s: CLOSED", this); } - else { Log.w(LOG_DOMAIN, "BTSocket %s: CLOSED err %d/%d '%s'", this, domain, code, message); } - - disconnect(); - - final CloseStatus status = new CloseStatus(domain, code, message); - synchronized (c4socket.getLock()) { - c4socket.closeCore(status); - } - } - - @SuppressWarnings("PMD.CloseResource") - private void disconnect() { - connecting = false; - final InputStream in = inputStream; - final OutputStream out = outputStream; - final BluetoothSocket s = btSocket; - - inputStream = null; - outputStream = null; - btSocket = null; - - if (in != null) { try { in.close(); } catch (IOException ignore) {} } - if (out != null) { try { out.close(); } catch (IOException ignore) {} } - if (s != null) { try { s.close(); } catch (IOException ignore) {} } - - readExecutor.shutdownNow(); - socketQueue.shutdown(); - } - - private boolean isReadThrottled() { - return receivedBytesPending >= MAX_RECEIVED_BYTES_PENDING; - } - - @Override - @NonNull - public String toString() { return "BTSocket[" + peerID + "]"; } -} diff --git a/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java b/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java deleted file mode 100644 index 8d19ee137..000000000 --- a/common/main/java/com/couchbase/lite/internal/core/impl/NativeC4BTSocketFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.couchbase.lite.internal.core.impl; - -import com.couchbase.lite.internal.core.C4BTSocketFactory; - -/** - * JNI implementation of {@link C4BTSocketFactory.NativeImpl}. - */ -public final class NativeC4BTSocketFactory implements C4BTSocketFactory.NativeImpl { - - // ------------------------------------------------------------------------- - // C4BTSocketFactory.NativeImpl - // ------------------------------------------------------------------------- - - @Override - public long nRegisterFactory() { - return registerBTSocketFactory(); - } - - @Override - public long nFromNative(long token, String scheme, String host, long port, String path, int framing) { - return fromNative(token, scheme, host, port, path, framing); - } - - // ------------------------------------------------------------------------- - // Native declarations - // ------------------------------------------------------------------------- - - /** Registers the BTSocketFactory with LiteCore and returns a context token. */ - private static native long registerBTSocketFactory(); - - /** Wrap an existing Java C4BTSocket in a C-native C4Socket (peripheral side). */ - private static native long fromNative(long token, String scheme, String host, long port, String path, int framing); -} From c5c9b8f9ab1198e31905d43377637e9309fafedb Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Sun, 15 Mar 2026 00:21:41 +0530 Subject: [PATCH 13/20] Updated Socketfactory's classes from java to android path --- common/main/cpp/native_c4btsocketfactory.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/main/cpp/native_c4btsocketfactory.cc b/common/main/cpp/native_c4btsocketfactory.cc index 295b6e0d8..e3d588ae6 100644 --- a/common/main/cpp/native_c4btsocketfactory.cc +++ b/common/main/cpp/native_c4btsocketfactory.cc @@ -26,7 +26,7 @@ static jmethodID m_attached; bool litecore::jni::initC4BTSocketFactory(JNIEnv* env) { jclass localCls = env->FindClass( - "com/couchbase/lite/internal/BluetoothSocketFactory"); + "com/couchbase/lite/internal/p2p/ble/BluetoothSocketFactory"); if (!localCls) return false; cls_C4BTSocketFactory = reinterpret_cast(env->NewGlobalRef(localCls)); From 85d2f8729e4c5a7d4b6892f0eb161421822c7ddb Mon Sep 17 00:00:00 2001 From: Aniket Singh Date: Fri, 20 Mar 2026 10:05:46 +0530 Subject: [PATCH 14/20] Hashmap update and initating the C4BtSocketFactory --- common/main/cpp/MetadataHelper.cc | 11 ++++++++++- ...internal_core_impl_NativeC4PeerDiscoveryProvider.h | 4 ++-- common/main/cpp/native_c4peerdiscoveryprovider.cc | 9 ++++++--- common/main/cpp/native_glue.cc | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/common/main/cpp/MetadataHelper.cc b/common/main/cpp/MetadataHelper.cc index 820640846..0b4010a23 100644 --- a/common/main/cpp/MetadataHelper.cc +++ b/common/main/cpp/MetadataHelper.cc @@ -50,7 +50,16 @@ namespace litecore::jni { // Create HashMap jclass hashMapClass = env->FindClass("java/util/HashMap"); jmethodID hashMapInit = env->GetMethodID(hashMapClass, "", "(I)V"); - jobject hashMap = env->NewObject(hashMapClass, hashMapInit); + + + size_t rawSize = metadata.size(); + jint size = (rawSize > 10000) ? 0 : static_cast(rawSize); + + jobject hashMap = env->NewObject(hashMapClass, hashMapInit, size); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + return env->NewObject(hashMapClass, hashMapInit, 0); + } // Put method for HashMap jmethodID hashMapPut = env->GetMethodID( diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h index 526fa25f1..f5ac8d9d9 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h @@ -4,7 +4,7 @@ #define COUCHBASE_LITE_JAVA_EE_ROOT_COM_COUCHBASE_LITE_INTERNAL_CORE_IMPL_NATIVEC4PEERDISCOVERYPROVIDER_H #ifdef __cplusplus -extern "C++" { +extern "C" { #endif JNIEXPORT jlong JNICALL @@ -50,7 +50,7 @@ JNIEXPORT jlongArray JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_peersWithProvider( JNIEnv *env, jclass thiz, jlong providerPtr); -JNIEXPORT jstring JNICALL +JNIEXPORT jbyteArray JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_serviceUuidFromPeerGroup( JNIEnv* env, jclass, jstring peerGroup); diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 1698ce720..9c8385629 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -127,6 +127,7 @@ namespace litecore::jni { jint envState = attachJVM(&env, "startBrowsing"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; + jniLog("Start Browsing"); // Call Java BLE service to start scanning env->CallStaticVoidMethod( @@ -162,6 +163,7 @@ namespace litecore::jni { return; jstring jDisplayName = UTF8ToJstring(env, displayName.data(), displayName.size()); + jniLog("Start Publishing"); jobject jMetadata = metadataToJavaMap(env, metadata); env->CallStaticVoidMethod( @@ -253,6 +255,7 @@ namespace litecore::jni { if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; + jniLog("Update Metadata"); jobject jMetadata = metadataToJavaMap(env, metadata); env->CallStaticVoidMethod( @@ -360,7 +363,7 @@ namespace litecore::p2p { #ifdef __cplusplus -extern "C++" { +extern "C" { #endif //------------------------------------------------------------------------- @@ -368,13 +371,13 @@ extern "C++" { //------------------------------------------------------------------------- -JNIEXPORT jstring JNICALL +JNIEXPORT jbyteArray JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider_serviceUuidFromPeerGroup( JNIEnv* env, jclass, jstring peerGroup) { std::string pg = JstringToUTF8(env, peerGroup); auto uuid = litecore::p2p::btle::ServiceUUIDFromPeerGroup(pg); C4Slice s = {&uuid, sizeof(uuid)}; - return toJString(env, s); + return toJByteArray(env, s); } JNIEXPORT jlong JNICALL diff --git a/common/main/cpp/native_glue.cc b/common/main/cpp/native_glue.cc index b3f80756e..408e141bd 100644 --- a/common/main/cpp/native_glue.cc +++ b/common/main/cpp/native_glue.cc @@ -163,6 +163,7 @@ JNI_OnLoad(JavaVM *jvm, void *ignore) { #ifdef __ANDROID__ && initC4MultipeerReplicator(env) && initC4PeerDiscoveryProvider(env) + && initC4BTSocketFactory(env) #endif #endif && initC4Socket(env)) { From bb4b679561185bb4dd642cf1325649a1ec260eec Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Tue, 24 Mar 2026 10:53:02 +0900 Subject: [PATCH 15/20] Changes needed to make it possible for me to build Also some clang tidy changes, and overall cleanup as well as ensuring the proper minimum API --- common/CMakeLists.txt | 5 +- common/main/cpp/native_bluetoothpeer.cc | 2 +- .../cpp/native_c4peerdiscoveryprovider.cc | 77 ++++++++----------- 3 files changed, 37 insertions(+), 47 deletions(-) diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 6c920ed77..9c451393a 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -97,6 +97,9 @@ target_link_options( target_link_libraries( LiteCoreJNI LiteCore + # Below are needed on Android. If this gets moved to Java as well this will need + # to be changed "log" + c++_static + c++abi ) - diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index 0a458bc6c..9ec256da7 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -75,7 +75,7 @@ extern "C++" { JNIEnv *env, jclass clazz, jlong peerPtr, jstring jurl) { std::string url = JstringToUTF8(env, jurl); - BluetoothPeer *peer = static_cast(getPeer(peerPtr)); + auto *peer = dynamic_cast(getPeer(peerPtr)); if (!peer) return; peer->resolvingUrl(url, kC4NoError); diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index 9c8385629..c75732016 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -7,15 +7,27 @@ #include "MetadataHelper.h" #include "TLSCodec.hh" #include "c4Socket.hh" +#include "c4Error.h" #include "native_bluetoothpeer_internal.h" #include "com_couchbase_lite_internal_core_impl_NativeC4BTSocketFactory.h" #include "com_couchbase_lite_internal_core_impl_NativeC4PeerDiscoveryProvider.h" +#include using namespace litecore; using namespace litecore::jni; using namespace litecore::p2p; +using namespace std; namespace litecore::jni { + static void assertAndroidLevel() { + constexpr int MIN_LEVEL = __ANDROID_API_Q__; + if (android_get_device_api_level() < MIN_LEVEL) { + C4Error::raise(LiteCoreDomain, kC4ErrorUnsupported, + "Bluetooth peer discovery requires Android API %d or higher", + MIN_LEVEL); + } + } + static jclass cls_C4PeerDiscoveryProvider; static jmethodID m_C4PeerDiscoveryProvider_startBrowsing; @@ -94,7 +106,8 @@ namespace litecore::jni { class C4BLEProvider : public C4PeerDiscoveryProvider { public: C4BLEProvider(C4PeerDiscovery& discovery, std::string_view peerGroupID) - : C4PeerDiscoveryProvider(discovery, kPeerSyncProtocol_BluetoothLE, peerGroupID) { + : C4PeerDiscoveryProvider(discovery, kPeerSyncProtocol_BluetoothLE, peerGroupID) + , _socket(litecore::jni::kBTSocketFactory){ std::string pg(peerGroupID); JNIEnv* env = nullptr; jint envState = attachJVM(&env, "initBleProvider"); @@ -102,7 +115,7 @@ namespace litecore::jni { jstring jPeerGroup = UTF8ToJstring(env, pg.data(), pg.size()); - jlong providerPtr = reinterpret_cast(this); + auto providerPtr = reinterpret_cast(this); jlong token = env->CallStaticLongMethod( cls_C4PeerDiscoveryProvider, m_C4PeerDiscoveryProvider_initBleProvider, @@ -115,14 +128,14 @@ namespace litecore::jni { token = 0; } _contextToken = token; - _socket = litecore::jni::kBTSocketFactory; _socket.context = this; if (envState == JNI_EDETACHED) detachJVM("initBleProvider"); if (jPeerGroup) env->DeleteLocalRef(jPeerGroup); } - virtual void startBrowsing() override { + void startBrowsing() override { + assertAndroidLevel(); JNIEnv *env = nullptr; jint envState = attachJVM(&env, "startBrowsing"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) @@ -155,8 +168,9 @@ namespace litecore::jni { } } - virtual void startPublishing(std::string_view displayName, uint16_t port, - C4Peer::Metadata const& metadata) override { + void startPublishing(std::string_view displayName, uint16_t port, + C4Peer::Metadata const& metadata) override { + assertAndroidLevel(); JNIEnv *env = nullptr; jint envState = attachJVM(&env, "startPublishing"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) @@ -197,14 +211,14 @@ namespace litecore::jni { } } - virtual void monitorMetadata(C4Peer* peer, bool start) override { + void monitorMetadata(C4Peer* peer, bool start) override { JNIEnv *env = nullptr; jint envState = attachJVM(&env, "monitorMetadata"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; jbyteArray peerId = toJByteArray(env, (const uint8_t*)peer->id.data(), peer->id.size()); - jlong peerPtr = reinterpret_cast(peer); + auto peerPtr = reinterpret_cast(peer); if (start) { @@ -230,13 +244,13 @@ namespace litecore::jni { } } - virtual void resolveURL(C4Peer* peer) override { + void resolveURL(C4Peer* peer) override { JNIEnv *env = nullptr; jint envState = attachJVM(&env, "resolveURL"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; - jlong peerPtr = reinterpret_cast(peer); + auto peerPtr = reinterpret_cast(peer); env->CallStaticVoidMethod( cls_C4PeerDiscoveryProvider, @@ -249,7 +263,7 @@ namespace litecore::jni { } } - virtual void updateMetadata(C4Peer::Metadata const& metadata) override { + void updateMetadata(C4Peer::Metadata const& metadata) override { JNIEnv *env = nullptr; jint envState = attachJVM(&env, "updateMetadata"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) @@ -271,13 +285,13 @@ namespace litecore::jni { } } - virtual void shutdown(std::function onComplete) override { + void shutdown(std::function onComplete) override { stopBrowsing(); stopPublishing(); onComplete(); } - virtual void stop(Mode mode) override { + void stop(Mode mode) override { if (mode == C4PeerDiscovery::Mode::browse) { stopBrowsing(); } @@ -290,7 +304,7 @@ namespace litecore::jni { return addPeer(peer, moreComing); } - void removeDiscoveredPeer(std::string id, bool moreComing = false) { + void removeDiscoveredPeer(const string &id, bool moreComing = false) { removePeer(id, moreComing); } @@ -298,10 +312,6 @@ namespace litecore::jni { statusChanged(m, s); } - void setContextToken(jlong token) { - _contextToken = token; - } - std::optional getSocketFactory() const override { return net::wrapSocketFactoryInTLS(_socket); } @@ -316,17 +326,17 @@ namespace litecore::jni { private: jlong _contextToken{}; - C4SocketFactory _socket; + C4SocketFactory _socket{}; }; // Factory function for creating BLE provider static std::unique_ptr createBLEProvider(C4PeerDiscovery& discovery, std::string_view peerGroupID) { - C4BLEProvider* provider = new C4BLEProvider(discovery, peerGroupID); - return C4PeerDiscovery::ProviderRef(provider, [](C4PeerDiscoveryProvider* ptr) { + auto* provider = new C4BLEProvider(discovery, peerGroupID); + return {provider, [](C4PeerDiscoveryProvider* ptr) { delete ptr; - }); + }}; } // Register the BLE provider @@ -338,29 +348,6 @@ namespace litecore::jni { static bool bleProviderRegistered = registerBleProvider(); } -namespace litecore::p2p { - - // BleP2pConstants - static jclass cls_BleP2pConstants; - - static jclass jUuidClass; - static jmethodID uuidFromString; - static jfieldID PORT_CHAR_FIELD, META_CHAR_FIELD, PEER_GROUP_NS_FIELD; - - - void setUuidConstant(JNIEnv* env, jclass cls, const char* uuidStr) { - jstring str = env->NewStringUTF(uuidStr); - jobject uuidObj = env->CallStaticObjectMethod(jUuidClass, uuidFromString, str); - - if (strcmp(uuidStr, litecore::p2p::btle::kPortCharacteristicID) == 0) - env->SetStaticObjectField(cls, PORT_CHAR_FIELD, uuidObj); - else if (strcmp(uuidStr, litecore::p2p::btle::kMetadataCharacteristicID) == 0) - env->SetStaticObjectField(cls, META_CHAR_FIELD, uuidObj); - else if (strcmp(uuidStr, litecore::p2p::btle::kPeerGroupUUIDNamespace) == 0) - env->SetStaticObjectField(cls, PEER_GROUP_NS_FIELD, uuidObj); - } -} - #ifdef __cplusplus extern "C" { From 524021226e4cdc773e471c2f16d8d215a38ad014 Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Thu, 26 Mar 2026 12:48:03 +0900 Subject: [PATCH 16/20] Delete dangling local ref --- common/main/cpp/MetadataHelper.cc | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/main/cpp/MetadataHelper.cc b/common/main/cpp/MetadataHelper.cc index 0b4010a23..15f7a7923 100644 --- a/common/main/cpp/MetadataHelper.cc +++ b/common/main/cpp/MetadataHelper.cc @@ -41,6 +41,9 @@ namespace litecore::jni { env->DeleteLocalRef(entrySet); env->DeleteLocalRef(entries); + env->DeleteLocalRef(mapClass); + env->DeleteLocalRef(setClass); + env->DeleteLocalRef(entryClass); return metadata; } From d20415b6e69f264a1f9840cebaa0aa3d01b5ceae Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Thu, 26 Mar 2026 13:27:29 +0900 Subject: [PATCH 17/20] Use byte array slice in its own scope Otherwise it will crash in the destructor because the local ref is deleted first --- common/main/cpp/MetadataHelper.cc | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/common/main/cpp/MetadataHelper.cc b/common/main/cpp/MetadataHelper.cc index 15f7a7923..5d2989b72 100644 --- a/common/main/cpp/MetadataHelper.cc +++ b/common/main/cpp/MetadataHelper.cc @@ -29,10 +29,12 @@ namespace litecore::jni { jstring jKey = (jstring)env->CallObjectMethod(entry, getKeyMethod); jbyteArray jValue = (jbyteArray)env->CallObjectMethod(entry, getValueMethod); - std::string key = JstringToUTF8(env, jKey); - jbyteArraySlice value(env, jValue); + { + std::string key = JstringToUTF8(env, jKey); + jbyteArraySlice value(env, jValue); - metadata[std::string(key)] = fleece::alloc_slice(value); + metadata[std::string(key)] = fleece::alloc_slice(value); + } env->DeleteLocalRef(entry); env->DeleteLocalRef(jKey); From 10e64ad004485dcc2a0b0e1e49bc57e6fe67967c Mon Sep 17 00:00:00 2001 From: Pasin Date: Wed, 25 Mar 2026 23:27:48 -0700 Subject: [PATCH 18/20] Fix JNI linkage in NativeBluetoothPeer Fixed JNI linkaged by changing extern "C++" to extern "C" to prevent UnsatisfiedLinkError. --- .../com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h | 2 +- common/main/cpp/native_bluetoothpeer.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h index 6d19aabae..262a45312 100644 --- a/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h +++ b/common/main/cpp/com_couchbase_lite_internal_core_impl_NativeBluetoothPeer.h @@ -6,7 +6,7 @@ #include #ifdef __cplusplus -extern "C++" { +extern "C" { #endif /* diff --git a/common/main/cpp/native_bluetoothpeer.cc b/common/main/cpp/native_bluetoothpeer.cc index 9ec256da7..d862c9c2c 100644 --- a/common/main/cpp/native_bluetoothpeer.cc +++ b/common/main/cpp/native_bluetoothpeer.cc @@ -16,7 +16,7 @@ static C4Peer* getPeer(jlong peerPtr) { return reinterpret_cast(peerPtr); } -extern "C++" { +extern "C" { JNIEXPORT jstring JNICALL Java_com_couchbase_lite_internal_core_impl_NativeBluetoothPeer_getId( JNIEnv *env, jclass clazz, jlong peerPtr) { From 43562e500aeb50c4ea1cdad0c498a8510117d19a Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Fri, 27 Mar 2026 10:18:01 +0900 Subject: [PATCH 19/20] Use the proper context and directly use string_view --- common/main/cpp/native_c4peerdiscoveryprovider.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/main/cpp/native_c4peerdiscoveryprovider.cc b/common/main/cpp/native_c4peerdiscoveryprovider.cc index c75732016..0601af2d6 100644 --- a/common/main/cpp/native_c4peerdiscoveryprovider.cc +++ b/common/main/cpp/native_c4peerdiscoveryprovider.cc @@ -108,12 +108,11 @@ namespace litecore::jni { C4BLEProvider(C4PeerDiscovery& discovery, std::string_view peerGroupID) : C4PeerDiscoveryProvider(discovery, kPeerSyncProtocol_BluetoothLE, peerGroupID) , _socket(litecore::jni::kBTSocketFactory){ - std::string pg(peerGroupID); JNIEnv* env = nullptr; jint envState = attachJVM(&env, "initBleProvider"); if ((envState != JNI_OK) && (envState != JNI_EDETACHED)) return; - jstring jPeerGroup = UTF8ToJstring(env, pg.data(), pg.size()); + jstring jPeerGroup = UTF8ToJstring(env, peerGroupID.data(), peerGroupID.size()); auto providerPtr = reinterpret_cast(this); jlong token = env->CallStaticLongMethod( @@ -128,7 +127,7 @@ namespace litecore::jni { token = 0; } _contextToken = token; - _socket.context = this; + _socket.context = reinterpret_cast(_contextToken); if (envState == JNI_EDETACHED) detachJVM("initBleProvider"); if (jPeerGroup) env->DeleteLocalRef(jPeerGroup); From 997cf22d5d1990e5e30e1dc811ec37a312559e31 Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Sat, 28 Mar 2026 13:55:22 +0900 Subject: [PATCH 20/20] TMP: register private key with undocumented method --- common/main/cpp/native_c4multipeerreplicator.cc | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/common/main/cpp/native_c4multipeerreplicator.cc b/common/main/cpp/native_c4multipeerreplicator.cc index 9db2bbd95..cfb189585 100644 --- a/common/main/cpp/native_c4multipeerreplicator.cc +++ b/common/main/cpp/native_c4multipeerreplicator.cc @@ -23,8 +23,16 @@ #include "native_glue.hh" #include "native_c4replutils.hh" #include "socket_factory.h" +#include "c4Certificate.hh" #include "com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator.h" +namespace litecore::net { + class TLSContext { + public: + static void registerPrivateKey(fleece::slice certData, crypto::PrivateKey* C4NULLABLE); + }; +} + using namespace litecore; using namespace litecore::jni; using namespace std; @@ -544,6 +552,10 @@ JNICALL Java_com_couchbase_lite_internal_core_impl_NativeC4MultipeerReplicator_c return 0; } params.tlsKeyPair = (keyPair == 0L) ? nullptr : (C4KeyPair *) keyPair; + if (params.protocols & kPeerSyncProtocol_BluetoothLE) { + jbyteArraySlice fleeceCert(env, cert); + litecore::net::TLSContext::registerPrivateKey((fleece::slice)fleeceCert, ((C4KeyPair *)keyPair)->getPrivateKey()); + } // Database and Collections: params.database = (C4Database *) c4db;