wear: address PR review - (1) DM send no longer echoes 'Sent' pre-handshake: session-gated echo stays 'Sending' and retries for 15s after initiating Noise handshake; (2) minSdk 33 (Wear OS 4): API 30 lacks the S+ Bluetooth permissions and would require location for scan, which the app refuses; (3) recreate BluetoothConnectionManager when !isReusable() so stop->start cycles don't leave a zombie mesh

This commit is contained in:
callebtc 2026-07-29 02:54:11 +02:00
parent 120cc8e901
commit e4ad09830b
4 changed files with 47 additions and 6 deletions

View File

@ -12,7 +12,9 @@ android {
defaultConfig {
applicationId = "com.bitchat.watch"
minSdk = 30 // Wear OS 3 (Pixel Watch 1); BLE APIs match the phone app's usage
minSdk = 33 // Wear OS 4 (Pixel Watch 1+): the S+ Bluetooth permissions the app
// declares only exist from API 31, and API 30 would additionally require location
// for BLE scan results, which the app deliberately refuses.
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
versionName = "0.1.0"

View File

@ -233,6 +233,7 @@ fun WearNavHost(openDmPeer: String?, onOpenDmHandled: () -> Unit) {
)
is WearScreen.TextInput -> {
val mesh = WearMeshService.peek()
val sendScope = androidx.compose.runtime.rememberCoroutineScope()
com.bitchat.watch.ui.TextInputScreen(
onSend = { text ->
mesh?.let { m ->
@ -240,7 +241,7 @@ fun WearNavHost(openDmPeer: String?, onOpenDmHandled: () -> Unit) {
sendPublicMessage(m, text)
} else {
val nick = m.getPeerNickname(current.peerID) ?: current.peerID
sendPrivateMessage(m, current.peerID, nick, text)
sendPrivateMessage(m, current.peerID, nick, text, sendScope)
}
}
goBack()

View File

@ -50,7 +50,7 @@ class WearMeshService private constructor(private val context: Context) {
private val bleTransport = BleTransport()
private val meshCore: MeshCore
private val connectionManager: BluetoothConnectionManager
private var connectionManager: BluetoothConnectionManager
@Volatile
var nickname: String = loadNickname()
@ -246,6 +246,15 @@ class WearMeshService private constructor(private val context: Context) {
Log.w(TAG, "Mesh already active, ignoring duplicate start")
return
}
if (!connectionManager.isReusable()) {
// A previous stopServices() cancelled the manager's coroutine scope; the shared
// API marks such managers single-use, so build a fresh one instead of starting
// a zombie mesh that reports active while scanning nothing.
Log.i(TAG, "Recreating BluetoothConnectionManager after terminal stop")
connectionManager = BluetoothConnectionManager(context, myPeerID, meshCore.fragmentManager)
bleTransport.connectionManager = connectionManager
wireBluetoothDelegate()
}
val started = connectionManager.startServices()
if (started) {
isActive = true

View File

@ -8,6 +8,9 @@ import com.bitchat.android.services.AppStateStore
import com.bitchat.watch.mesh.WearMeshService
import java.io.File
import java.util.Date
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal fun sendPublicMessage(mesh: WearMeshService, content: String) {
mesh.sendMessage(content)
@ -22,23 +25,49 @@ internal fun sendPublicMessage(mesh: WearMeshService, content: String) {
)
}
/**
* DM send with honest delivery state. MeshCore drops pre-handshake content (it only kicks
* off the Noise handshake), so when no session exists we must not echo "Sent": the echo
* stays "Sending" while a retry loop waits for the session and completes the send.
*/
internal fun sendPrivateMessage(
mesh: WearMeshService,
peerID: String,
recipientNickname: String,
content: String
content: String,
scope: CoroutineScope
) {
mesh.sendPrivateMessage(content, peerID, recipientNickname)
val established = mesh.hasEstablishedSession(peerID)
val messageID = java.util.UUID.randomUUID().toString()
if (established) {
mesh.sendPrivateMessageWithId(content, peerID, recipientNickname, messageID)
} else {
mesh.initiateNoiseHandshake(peerID)
scope.launch {
val deadline = System.currentTimeMillis() + 15_000
while (System.currentTimeMillis() < deadline) {
if (mesh.hasEstablishedSession(peerID)) {
mesh.sendPrivateMessageWithId(content, peerID, recipientNickname, messageID)
AppStateStore.updatePrivateMessageStatus(messageID, DeliveryStatus.Sent)
return@launch
}
delay(400)
}
// Session never came up: the echo honestly stays "Sending" (AppStateStore
// refuses status downgrades, so it cannot be marked Failed from here).
}
}
AppStateStore.addPrivateMessage(
peerID,
BitchatMessage(
id = messageID,
sender = mesh.nickname,
content = content,
timestamp = Date(),
isPrivate = true,
recipientNickname = recipientNickname,
senderPeerID = mesh.myPeerID,
deliveryStatus = DeliveryStatus.Sent
deliveryStatus = if (established) DeliveryStatus.Sent else DeliveryStatus.Sending
)
)
}