diff --git a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt index 7bc534b..b76fc8d 100644 --- a/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt +++ b/app/src/main/java/com/medithings/vesiscan/ble/BleManager.kt @@ -201,6 +201,10 @@ class BleManager private constructor(private val context: Context) { * ClinicalHome 등에서 "BLE Connecting…" 표시용. */ val isConnecting = mutableStateOf(false) val connectionError = mutableStateOf(null) + /** 마지막으로 GATT status=5 (bond mismatch) 로 실패한 device 주소. + * FW allowlist 1:1 bond 정책에서 다른 central 이 붙어 있는 경우 발생. + * UI 가 이 값 non-null 시 "본딩 초기화" action 을 노출. 성공/타 에러로 clear. */ + val bondConflictAddress = mutableStateOf(null) // Callbacks var onConnectionStateChanged: ((Boolean) -> Unit)? = null @@ -1145,7 +1149,17 @@ class BleManager private constructor(private val context: Context) { scheduleAutoReconnect() } else { isReconnecting.value = false - connectionError.value = "Connection failed (status $status). Try again." + // GATT status=5 (INSUFFICIENT_AUTHENTICATION) — FW allowlist 정책상 + // 다른 central 과 이미 bond 중이면 peripheral 이 인증 거부. + val failedAddr = gatt.device.address + if (status == 5) { + bondConflictAddress.value = failedAddr + connectionError.value = "BOND_CONFLICT:$failedAddr" + debugLogger.warn("BOND_CONFLICT addr=$failedAddr — FW allowlist rejected (status 5)") + } else { + bondConflictAddress.value = null + connectionError.value = "Connection failed (status $status). Try again." + } } return@post } @@ -1157,6 +1171,7 @@ class BleManager private constructor(private val context: Context) { isConnecting.value = false isUserDisconnect = false connectionError.value = null + bondConflictAddress.value = null // 연결 성공 시 클리어 val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address } if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = true, isConnecting = false) isConnected.value = true diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt index 2193dfe..03fcaa8 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/connection/DeviceScanView.kt @@ -68,6 +68,7 @@ fun DeviceScanView(appState: AppState) { val isScanning by bleManager.isScanning val bluetoothEnabled by bleManager.bluetoothEnabled val connectionError by bleManager.connectionError + val bondConflictAddress by bleManager.bondConflictAddress // BLE permissions val requiredPermissions = remember { @@ -225,22 +226,55 @@ fun DeviceScanView(appState: AppState) { Spacer(modifier = Modifier.height(8.dp)) } - // Connection error - connectionError?.let { error -> - Row( + // Connection error — GATT status=5 (bond conflict) 시 별도 UX 안내 + reset action. + val curError = connectionError + val curBondAddr = bondConflictAddress + if (curError != null) { + val isBondConflict = curError.startsWith("BOND_CONFLICT:") && curBondAddr != null + Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) .clip(RoundedCornerShape(12.dp)) .background(Color.Red) - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically + .padding(12.dp) ) { - Icon(Icons.Default.Close, stringResource(R.string.device_scan_error_desc), tint = Color.White) - Spacer(modifier = Modifier.width(12.dp)) - Text(error, color = Color.White, fontSize = 13.sp, modifier = Modifier.weight(1f)) - IconButton(onClick = { bleManager.connectionError.value = null }) { - Icon(Icons.Default.Close, stringResource(R.string.device_scan_dismiss_desc), tint = Color.White.copy(alpha = 0.8f)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Default.Close, stringResource(R.string.device_scan_error_desc), tint = Color.White) + Spacer(modifier = Modifier.width(12.dp)) + val displayText = if (isBondConflict) + stringResource(R.string.ble_error_bond_conflict) + else + curError + Text(displayText, color = Color.White, fontSize = 13.sp, modifier = Modifier.weight(1f)) + IconButton(onClick = { + bleManager.connectionError.value = null + bleManager.bondConflictAddress.value = null + }) { + Icon(Icons.Default.Close, stringResource(R.string.device_scan_dismiss_desc), tint = Color.White.copy(alpha = 0.8f)) + } + } + if (isBondConflict && curBondAddr != null) { + Spacer(modifier = Modifier.height(6.dp)) + OutlinedButton( + onClick = { + bleManager.unbondAndRemoveAddress(curBondAddr) + bleManager.connectionError.value = null + bleManager.bondConflictAddress.value = null + android.widget.Toast.makeText( + context, + context.getString(R.string.ble_toast_bond_removed), + android.widget.Toast.LENGTH_LONG, + ).show() + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Color.White, + ), + border = androidx.compose.foundation.BorderStroke(1.dp, Color.White), + modifier = Modifier.align(Alignment.End), + ) { + Text(stringResource(R.string.ble_action_reset_bond), fontSize = 12.sp) + } } } Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 5a105e0..015df0e 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -379,6 +379,10 @@ 페어링 필요 블루투스 페어링 요청이 표시됩니다.\n\'페어링\' 을 누르고, PIN 이 나타나면 입력하세요. 페어링이 필요합니다. 기기를 페어링해주세요. + + 이 기기는 다른 앱과 이미 페어링되어 있습니다. 기기 전원 버튼을 15초 롱프레스로 본딩 정보를 삭제한 뒤 다시 시도하세요. + 이 폰의 본딩 정보 초기화 + 본딩 정보 삭제 완료. 다시 연결을 시도하세요. 뒤로 경고 오류 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fe32a1e..7c06ef3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -379,6 +379,10 @@ Pairing Required A Bluetooth pairing request will appear.\nTap \'Pair\', then enter PIN if prompted. Pairing required. Please pair the device. + + This device is already paired with another app. Long-press the device power button for 15 seconds to clear its pairing, then try again. + Reset Pairing (this phone) + Pairing info removed. Please try connecting again. Back Warning Error