feat(ble): GATT status=5 (FW allowlist bond conflict) UX (cloud-mvp 10c45dc 이식)
FW 팀이 1:1 bond allowlist 정책 추가 예정. 다른 central 과 이미 bond 중인
기기에 붙으면 GATT_INSUFFICIENT_AUTHENTICATION (status=5) 로 거부.
BleManager:
- bondConflictAddress: MutableStateOf<String?> 신규
- status=5 감지 시 bondConflictAddress=addr + connectionError="BOND_CONFLICT:$addr"
- CONNECTED 성공 시 clear
DeviceScanView (ui/views/connection/):
- 에러 배너 확장 (Column 2단):
· 안내 문구 (기기 15초 롱프레스로 본딩 삭제)
· "이 폰의 본딩 정보 초기화" OutlinedButton (unbondAndRemoveAddress)
- Toast 완료 안내
strings.xml (KR/EN): ble_error_bond_conflict / ble_action_reset_bond / ble_toast_bond_removed
This commit is contained in:
@@ -201,6 +201,10 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
* ClinicalHome 등에서 "BLE Connecting…" 표시용. */
|
* ClinicalHome 등에서 "BLE Connecting…" 표시용. */
|
||||||
val isConnecting = mutableStateOf(false)
|
val isConnecting = mutableStateOf(false)
|
||||||
val connectionError = mutableStateOf<String?>(null)
|
val connectionError = mutableStateOf<String?>(null)
|
||||||
|
/** 마지막으로 GATT status=5 (bond mismatch) 로 실패한 device 주소.
|
||||||
|
* FW allowlist 1:1 bond 정책에서 다른 central 이 붙어 있는 경우 발생.
|
||||||
|
* UI 가 이 값 non-null 시 "본딩 초기화" action 을 노출. 성공/타 에러로 clear. */
|
||||||
|
val bondConflictAddress = mutableStateOf<String?>(null)
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
var onConnectionStateChanged: ((Boolean) -> Unit)? = null
|
var onConnectionStateChanged: ((Boolean) -> Unit)? = null
|
||||||
@@ -1145,7 +1149,17 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
scheduleAutoReconnect()
|
scheduleAutoReconnect()
|
||||||
} else {
|
} else {
|
||||||
isReconnecting.value = false
|
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
|
return@post
|
||||||
}
|
}
|
||||||
@@ -1157,6 +1171,7 @@ class BleManager private constructor(private val context: Context) {
|
|||||||
isConnecting.value = false
|
isConnecting.value = false
|
||||||
isUserDisconnect = false
|
isUserDisconnect = false
|
||||||
connectionError.value = null
|
connectionError.value = null
|
||||||
|
bondConflictAddress.value = null // 연결 성공 시 클리어
|
||||||
val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address }
|
val idx = discoveredDevices.indexOfFirst { it.address == gatt.device.address }
|
||||||
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = true, isConnecting = false)
|
if (idx >= 0) discoveredDevices[idx] = discoveredDevices[idx].copy(isConnected = true, isConnecting = false)
|
||||||
isConnected.value = true
|
isConnected.value = true
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
val isScanning by bleManager.isScanning
|
val isScanning by bleManager.isScanning
|
||||||
val bluetoothEnabled by bleManager.bluetoothEnabled
|
val bluetoothEnabled by bleManager.bluetoothEnabled
|
||||||
val connectionError by bleManager.connectionError
|
val connectionError by bleManager.connectionError
|
||||||
|
val bondConflictAddress by bleManager.bondConflictAddress
|
||||||
|
|
||||||
// BLE permissions
|
// BLE permissions
|
||||||
val requiredPermissions = remember {
|
val requiredPermissions = remember {
|
||||||
@@ -225,22 +226,55 @@ fun DeviceScanView(appState: AppState) {
|
|||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection error
|
// Connection error — GATT status=5 (bond conflict) 시 별도 UX 안내 + reset action.
|
||||||
connectionError?.let { error ->
|
val curError = connectionError
|
||||||
Row(
|
val curBondAddr = bondConflictAddress
|
||||||
|
if (curError != null) {
|
||||||
|
val isBondConflict = curError.startsWith("BOND_CONFLICT:") && curBondAddr != null
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.background(Color.Red)
|
.background(Color.Red)
|
||||||
.padding(12.dp),
|
.padding(12.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Icon(Icons.Default.Close, stringResource(R.string.device_scan_error_desc), tint = Color.White)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Icon(Icons.Default.Close, stringResource(R.string.device_scan_error_desc), tint = Color.White)
|
||||||
Text(error, color = Color.White, fontSize = 13.sp, modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
IconButton(onClick = { bleManager.connectionError.value = null }) {
|
val displayText = if (isBondConflict)
|
||||||
Icon(Icons.Default.Close, stringResource(R.string.device_scan_dismiss_desc), tint = Color.White.copy(alpha = 0.8f))
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|||||||
@@ -379,6 +379,10 @@
|
|||||||
<string name="device_scan_pairing_required_title">페어링 필요</string>
|
<string name="device_scan_pairing_required_title">페어링 필요</string>
|
||||||
<string name="device_scan_pairing_required_msg">블루투스 페어링 요청이 표시됩니다.\n\'페어링\' 을 누르고, PIN 이 나타나면 입력하세요.</string>
|
<string name="device_scan_pairing_required_msg">블루투스 페어링 요청이 표시됩니다.\n\'페어링\' 을 누르고, PIN 이 나타나면 입력하세요.</string>
|
||||||
<string name="device_scan_pairing_required_err">페어링이 필요합니다. 기기를 페어링해주세요.</string>
|
<string name="device_scan_pairing_required_err">페어링이 필요합니다. 기기를 페어링해주세요.</string>
|
||||||
|
<!-- FW allowlist bond conflict (GATT status=5) — 2026-08-04 -->
|
||||||
|
<string name="ble_error_bond_conflict">이 기기는 다른 앱과 이미 페어링되어 있습니다. 기기 전원 버튼을 15초 롱프레스로 본딩 정보를 삭제한 뒤 다시 시도하세요.</string>
|
||||||
|
<string name="ble_action_reset_bond">이 폰의 본딩 정보 초기화</string>
|
||||||
|
<string name="ble_toast_bond_removed">본딩 정보 삭제 완료. 다시 연결을 시도하세요.</string>
|
||||||
<string name="device_scan_back_desc">뒤로</string>
|
<string name="device_scan_back_desc">뒤로</string>
|
||||||
<string name="device_scan_warning_desc">경고</string>
|
<string name="device_scan_warning_desc">경고</string>
|
||||||
<string name="device_scan_error_desc">오류</string>
|
<string name="device_scan_error_desc">오류</string>
|
||||||
|
|||||||
@@ -379,6 +379,10 @@
|
|||||||
<string name="device_scan_pairing_required_title">Pairing Required</string>
|
<string name="device_scan_pairing_required_title">Pairing Required</string>
|
||||||
<string name="device_scan_pairing_required_msg">A Bluetooth pairing request will appear.\nTap \'Pair\', then enter PIN if prompted.</string>
|
<string name="device_scan_pairing_required_msg">A Bluetooth pairing request will appear.\nTap \'Pair\', then enter PIN if prompted.</string>
|
||||||
<string name="device_scan_pairing_required_err">Pairing required. Please pair the device.</string>
|
<string name="device_scan_pairing_required_err">Pairing required. Please pair the device.</string>
|
||||||
|
<!-- FW allowlist bond conflict (GATT status=5) — 2026-08-04 -->
|
||||||
|
<string name="ble_error_bond_conflict">This device is already paired with another app. Long-press the device power button for 15 seconds to clear its pairing, then try again.</string>
|
||||||
|
<string name="ble_action_reset_bond">Reset Pairing (this phone)</string>
|
||||||
|
<string name="ble_toast_bond_removed">Pairing info removed. Please try connecting again.</string>
|
||||||
<string name="device_scan_back_desc">Back</string>
|
<string name="device_scan_back_desc">Back</string>
|
||||||
<string name="device_scan_warning_desc">Warning</string>
|
<string name="device_scan_warning_desc">Warning</string>
|
||||||
<string name="device_scan_error_desc">Error</string>
|
<string name="device_scan_error_desc">Error</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user