feat(dev): 앱 종료 시 자동 페어링 해제 (다중 폰×기기 테스트 workflow 자동화)

배경 (사용자 관찰):
  · 폰 A ↔ 기기 A 페어링 후 앱 종료 · 폰 B 로 기기 A 접근 시 실패.
  · 폰 B 에서 "저장된 기기" 삭제해도 기기 A 는 폰 A bond 만 신뢰 · 물리 리셋 필요.
  · 매번 수동으로 Settings > "페어링 해제" 누르기 번거로움 · 잊기 쉬움.

수정:
  1. GreenZoneConstants.autoUnbondOnAppExit: Boolean = false (신규)
     dev-mode 용 flag · 앱 종료 시 자동 unbond 여부.
  2. MainActivity.onDestroy() 훅
     flag ON + 연결된 기기 있으면 → BleManager.disconnectAndUnbond() 자동 실행.
     기기측 msr(unbond+reboot) + 폰측 removeBond · fresh state 유지.
  3. PiezoMonitoringView Settings (dev-mode 조건부)
     "Delete Pairing" 버튼 아래에 토글 카드: "앱 종료 시 자동 페어링 해제"
     설명: "[dev] 다중 폰×기기 테스트용 · 종료 시 unbond"

효과:
  · 폰 A 앱 종료 → 자동 unbond msr 전송 → 기기 A clean
  · 폰 B 접근 시 fresh pairing 즉시 성공
  · 매뉴얼 clean 도 여전히 Delete Pairing 버튼으로 즉시 실행 가능 (기존)

주의:
  · default OFF (일반 사용자 flow 는 bond 유지가 UX 이점)
  · onDestroy 는 앱 강제종료 시 스킵 가능 · 정상 종료 (뒤로가기/스와이프) 는 커버
  · try/catch silent · 종료 시점 실패는 UC-05 (필수기능 아님)

빌드: BUILD SUCCESSFUL 1m 15s.
This commit is contained in:
2026-08-13 11:41:53 +09:00
parent 10d7932ab2
commit 464bbfff06
3 changed files with 67 additions and 0 deletions
@@ -82,6 +82,28 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
/**
* 2026-08-12: 테스트 세션 종료 시 자동 페어링 해제 (dev-mode 용).
*
* `GreenZoneConstants.autoUnbondOnAppExit` ON 이고 · 연결된 기기가 있으면
* `disconnectAndUnbond()` 실행 → 기기측 msr(unbond+reboot) + 폰측 removeBond.
* 다음 실행 (다른 폰) 에서 fresh pairing 가능하도록 clean state 유지.
*
* onDestroy 는 앱 강제종료 시 스킵될 수 있으나 · 정상 종료 (뒤로가기 · 홈 스와이프
* 후 recent 앱에서 swipe away) 는 커버. dev-mode 테스트 workflow 는 대부분 정상 종료.
*/
override fun onDestroy() {
try {
if (com.medithings.vesiscan.managers.GreenZoneConstants.autoUnbondOnAppExit) {
val ble = com.medithings.vesiscan.ble.BleManager.getInstance(applicationContext)
if (ble.isConnected.value) {
ble.disconnectAndUnbond()
}
}
} catch (_: Exception) { /* silent · 종료 시점 · UC-05 */ }
super.onDestroy()
}
} }
@Composable @Composable
@@ -71,6 +71,20 @@ object GreenZoneConstants {
@Volatile var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE @Volatile var placementGuideMode: PlacementGuideMode = PlacementGuideMode.SIMPLE
@Volatile var alignmentAlgo: AlignmentAlgo = AlignmentAlgo.V1 // default V1 (일반 사용자). V2 (Method D) 는 clinical alignment session 전용. @Volatile var alignmentAlgo: AlignmentAlgo = AlignmentAlgo.V1 // default V1 (일반 사용자). V2 (Method D) 는 clinical alignment session 전용.
/**
* 2026-08-12: 테스트 세션 종료 시 자동 페어링 해제 (dev-mode 용).
*
* ON 시 · MainActivity.onDestroy() 에서 연결된 기기가 있으면 자동으로
* disconnectAndUnbond() 호출. 여러 폰 × 여러 기기 테스트 환경에서 매번 수동으로
* 페어링 해제할 필요 없이 앱 종료만 하면 기기 clean state 유지.
*
* 시나리오: 폰 A 로 기기 A 테스트 → 앱 종료 → 자동 unbond msr 전송 →
* 기기 A 는 폰 A bond 삭제 → 다음에 폰 B 가 기기 A 와 fresh pairing 성공.
*
* default OFF (일반 사용자 flow 는 bond 유지가 UX 이점 · 잘못 켜지면 매번 재페어링).
*/
@Volatile var autoUnbondOnAppExit: Boolean = false
/** Posture pitch 임계값 (deg). |pitch| > 이 값 → LYING, ≤ 이 값 → NON_LYING. dev 모드에서 조정 가능. */ /** Posture pitch 임계값 (deg). |pitch| > 이 값 → LYING, ≤ 이 값 → NON_LYING. dev 모드에서 조정 가능. */
@Volatile var pitchLyingThreshold: Float = 25f @Volatile var pitchLyingThreshold: Float = 25f
@@ -2403,6 +2403,37 @@ fun PiezoMonitoringView(appState: AppState) {
) )
} }
// 2026-08-12: 다중 폰 × 다중 기기 테스트 workflow 자동화 (dev-mode 전용).
// ON 시 · MainActivity.onDestroy() 에서 자동으로 disconnectAndUnbond().
// 다음 실행 (다른 폰) 에서 fresh pairing 성공 · 매번 수동 unbond 불필요.
if (appState.isDevMode) {
Spacer(modifier = Modifier.height(6.dp))
var autoUnbond by remember {
mutableStateOf(com.medithings.vesiscan.managers.GreenZoneConstants.autoUnbondOnAppExit)
}
Row(
modifier = Modifier.fillMaxWidth()
.background(MlPrimary.copy(alpha = 0.05f), RoundedCornerShape(10.dp))
.padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text("앱 종료 시 자동 페어링 해제",
fontSize = 13.sp, fontWeight = FontWeight.SemiBold)
Text("[dev] 다중 폰×기기 테스트용 · 종료 시 unbond",
fontSize = 10.sp, color = MlSecondaryText)
}
Switch(
checked = autoUnbond,
onCheckedChange = {
autoUnbond = it
com.medithings.vesiscan.managers.GreenZoneConstants.autoUnbondOnAppExit = it
},
colors = SwitchDefaults.colors(checkedTrackColor = MlPrimary),
)
}
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding)) HorizontalDivider(modifier = Modifier.padding(vertical = dividerPadding))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))