Files
VesiscanClinicalAndroid/app/src/main/java/com/medithings/vesiscan/AppState.kt
T
dw.jang 26dc53b4eb chore(cleanup): YOLO/CameraX/OCR 세트 완전 제거 (Category B · 사용자 요청)
배경: 소변컵 사진 촬영 → YOLO 검출 → MLKit OCR 눈금 인식 기능 폐기.
현재 미사용. Piezo 방광 측정 flow 만 실사용.

완전 제거 (4 파일 + 1 asset):
  · measure/YoloDetector.kt (130 L · ONNX YOLO)
  · measure/SimpleMeasureService.kt (287 L · MLKit OCR + 3-zone)
  · measure/CCPosition.kt (8 L)
  · ui/views/monitoring/UrineCameraScreen.kt (285 L · CameraX 프리뷰)
  · assets/urinecup_best.onnx (~10.6 MB · APK 크기 절감)

Gradle deps 삭제 (build.gradle.kts):
  · com.microsoft.onnxruntime:onnxruntime-android:1.17.0
  · com.google.mlkit:text-recognition:16.0.1
  · androidx.camera:camera-{core,camera2,lifecycle,view}:1.3.4

AndroidManifest.xml 삭제:
  · CAMERA permission
  · android.hardware.camera uses-feature

부분 편집:
  · AppState.kt        — URINE_CAMERA enum value 삭제
  · MainActivity.kt    — import + back-handler branch + Crossfade branch 삭제
  · PiezoMonitoringView.kt — showUrineCamera state · 카메라 카드 (16L) ·
                             LaunchedEffect(showUrineCamera) 삭제
  · strings.xml × 2    — 카메라 전용 문자열 15개 삭제
                        (camera_input 은 CatheterizeSheet 사용 중이라 유지)

측정용 VoidingRecord.kt 는 유지 (CatheterizeSheet · VoidingDiaryView 등에서 사용).
okhttp 는 speech/HfVolumeExtractor 에서 사용 중이라 유지.

합계: 약 780 LOC + 10.6 MB APK 크기 절감.
빌드: BUILD SUCCESSFUL 30s (첫 시도 통과).
2026-08-12 15:49:27 +09:00

316 lines
12 KiB
Kotlin

package com.medithings.vesiscan
import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.medithings.vesiscan.ble.BleManager
import com.medithings.vesiscan.models.*
import com.medithings.vesiscan.services.*
enum class AppScreen {
ONBOARDING, CONSENT, REGISTRATION, HOME, SENSOR_SELECT, DEVICE_SCAN,
PIEZO_PERSONALIZATION,
PLACEMENT_GUIDE, PIN_SETUP, PIN_ENTRY,
PIEZO_MONITORING, MEASUREMENT_HISTORY,
VOIDING_DIARY, REMINDER_SETTINGS,
CLINICAL_HOME, // ★ Dev 모드 한정 — 임상 R&D 측정 세션 화면 (입력 폼)
CLINICAL_LIVE, // ★ Dev 모드 한정 — 6채널 라이브 시각화 + Capture
FIRMWARE_UPDATE // ★ 2026-08-03 (cloud-mvp 672e679 이식) — VBT 프로브 firmware DFU
}
class AppState(private val context: Context) {
var currentScreen by mutableStateOf(AppScreen.ONBOARDING)
var user by mutableStateOf(User())
var sensorMode by mutableStateOf(SensorMode.PIEZO)
var bladderLevel by mutableStateOf(BladderLevel())
/**
* BLE 연결 상태 — **BleManager 실시간 값을 즉시 반영** (2026-08-03 fix · cloud-mvp 9c82a50).
*
* 이전: `mutableStateOf(false)` 로 stale snapshot 저장 → BLE 가 끊겨도 홈 화면이
* "연결됨" 으로 표시되고, Start 누르면 실제 PIEZO_MONITORING 에서
* "연결 해제" 로 표시되는 불일치 문제.
*
* 현재: getter 가 BleManager.isConnected 를 읽음 → Compose 가 자동 재구성.
* 기존 코드의 setter 는 no-op 로 유지 (호환).
*/
var isDeviceConnected: Boolean
get() = isDemoMode || bleManager.isConnected.value
set(_) { /* no-op · 실제 상태는 BleManager 가 관리 */ }
var isDemoMode by mutableStateOf(false)
var isDevMode by mutableStateOf(false)
var piezoSettings by mutableStateOf(PiezoSettings())
var catheterCount = mutableIntStateOf(0)
// Placement 진입 경로 분기용 — Monitoring에서 재진입 시 true, Personalization에서 진입 시 false
var placementFromMonitoring by mutableStateOf(false)
private val storage = UserStorage.getInstance(context)
private val historyStore = MeasurementHistoryStore.getInstance(context)
private val notificationService = NotificationService.getInstance(context)
private val bleManager = BleManager.getInstance(context)
private val reminderService = ReminderService.getInstance(context)
init {
loadState()
}
fun loadState() {
user = storage.user
sensorMode = storage.sensorMode
bladderLevel = bladderLevel.copy(
currentLevel = storage.bladderLevel,
catheterizationThreshold = storage.catheterizationLevel
)
piezoSettings = storage.piezoSettings
catheterCount.intValue = storage.catheterCount
currentScreen = when {
!storage.hasCompletedOnboarding -> AppScreen.ONBOARDING
!storage.isRegistered -> AppScreen.REGISTRATION
!storage.hasPin -> AppScreen.PIN_SETUP
else -> AppScreen.PIN_ENTRY
}
}
fun completeOnboarding() {
storage.hasCompletedOnboarding = true
storage.hasConsented = true
currentScreen = AppScreen.REGISTRATION
}
fun completeConsent() {
storage.hasConsented = true
storage.consentTimestamp = System.currentTimeMillis()
currentScreen = AppScreen.REGISTRATION
}
fun completeRegistration() {
storage.user = user
storage.isRegistered = true
currentScreen = AppScreen.PIN_SETUP
}
fun goHome() {
// 회의 결정: 실제 BLE 연결은 HOME에서도 유지. 다음 Start 시 설정/scan skip하고
// 바로 PiezoMonitoring으로 진입할 수 있도록.
// Demo 모드는 가짜 연결이므로 isDeviceConnected reset.
if (isDemoMode) isDeviceConnected = false
isDemoMode = false
currentScreen = AppScreen.HOME
}
fun enterDemoMode() {
isDemoMode = true
isDeviceConnected = true
currentScreen = AppScreen.PIEZO_MONITORING
}
fun startFromHome() {
sensorMode = SensorMode.PIEZO
storage.sensorMode = SensorMode.PIEZO
// 회의 결정: 이미 연결된 상태이면 설정/Placement skip하고 바로 도넛차트.
// 연결되지 않은 상태에서만 DeviceScan부터 일련의 과정 진행.
currentScreen = if (isDeviceConnected) AppScreen.PIEZO_MONITORING else AppScreen.DEVICE_SCAN
}
fun selectSensor(mode: SensorMode) {
sensorMode = mode
storage.sensorMode = mode
currentScreen = AppScreen.DEVICE_SCAN
}
fun backToSensorSelect() {
currentScreen = AppScreen.HOME
}
fun deviceConnected() {
isDeviceConnected = true
// 임상 모드에서 BLE 연결 시 ClinicalHome으로 복귀 (Personalization 우회)
if (com.medithings.vesiscan.services.ClinicalSessionStore.inClinicalFlow) {
currentScreen = AppScreen.CLINICAL_HOME
return
}
currentScreen = AppScreen.PIEZO_PERSONALIZATION
}
fun personalizationComplete() {
storage.piezoSettings = piezoSettings
placementFromMonitoring = false
// 회의 #8: AutoScanInterval을 Personalization 페이지에 통합 — 별도 화면 폐기.
currentScreen = AppScreen.PLACEMENT_GUIDE
}
/** Persist piezoSettings (called from in-session settings save). */
fun savePiezoSettingsNow() {
// Re-assign to trigger the mutableStateOf delegate so any composables
// observing piezoSettings recompose with the latest value, then persist
// to disk so the change survives process death.
piezoSettings = piezoSettings.copy()
storage.piezoSettings = piezoSettings
}
fun placementComplete() {
bladderLevel = BladderLevel(currentLevel = 0, catheterizationThreshold = bladderLevel.catheterizationThreshold)
storage.bladderLevel = 0
placementFromMonitoring = false
currentScreen = AppScreen.PIEZO_MONITORING
}
fun enterPlacementFromMonitoring() {
placementFromMonitoring = true
currentScreen = AppScreen.PLACEMENT_GUIDE
}
/** Clinical Mode alignment session 진입 — PlacementGuide 로 이동.
* ClinicalSessionStore.startMeasurement(mode=ALIGNMENT) 가 먼저 호출돼 있어야 함. */
fun enterAlignmentSession() {
placementFromMonitoring = false
currentScreen = AppScreen.PLACEMENT_GUIDE
}
fun backFromPlacementGuide() {
// 회의 #8: AutoScan을 Personalization으로 통합했으므로 Placement Back은
// 바로 Personalization으로 (이전 AUTO_SCAN_SETUP 단계 폐기).
currentScreen = if (placementFromMonitoring) AppScreen.PIEZO_MONITORING
else AppScreen.PIEZO_PERSONALIZATION
placementFromMonitoring = false
}
fun pinSet(pin: String) {
storage.pin = pin
storage.hasPin = true
currentScreen = AppScreen.HOME
}
fun pinVerified() {
currentScreen = AppScreen.HOME
}
fun incrementLevel() {
// 데모 · 1단계 증가 — 신규 함수로 위임 (cloud-mvp deb7b17 이식).
val currentVolume = estimatedVolume
val stepVolume = piezoSettings.maxVolume.toDouble() / BladderLevel.MAX_LEVEL
updateLevelFromMeasurement(currentVolume + stepVolume, recordHistory = true)
}
/**
* Phase A · 실측 경로 통합 (GUARDIAN-01, 2026-08-03 · cloud-mvp deb7b17 이식).
*
* 이전: PiezoMonitoringView 3곳 bladderLevel 직접 대입 →
* NotificationService · MeasurementHistoryStore 우회 → 실사용 시 알람 미발생.
* 이제: 실측/데모 경로 모두 이 함수로 일원화. 알람 · 이력 자동.
*
* @param volumeMl 실측 부피 (mL)
* @param recordHistory 이력 저장 여부 (기본 true · auto-scan 갱신 시 false 로 폭탄 방지)
*/
fun updateLevelFromMeasurement(volumeMl: Double, recordHistory: Boolean = true) {
val previousUrgency = bladderLevel.urgency
val maxVol = piezoSettings.maxVolume.toDouble()
val pct = if (maxVol > 0) (volumeMl / maxVol).coerceAtMost(1.0) else 0.0
val level = (pct * BladderLevel.MAX_LEVEL).toInt().coerceIn(0, BladderLevel.MAX_LEVEL)
bladderLevel = bladderLevel.copy(currentLevel = level)
storage.bladderLevel = bladderLevel.currentLevel
if (recordHistory) {
val record = MeasurementRecord(
eventType = MeasurementEventType.MEASUREMENT,
level = bladderLevel.currentLevel,
estimatedVolume = volumeMl.toInt(),
urgencyLevel = bladderLevel.urgency,
fillPercentage = fillPercentage,
)
historyStore.addRecord(record)
}
val newUrgency = bladderLevel.urgency
val urgencyChanged = newUrgency != previousUrgency
if (urgencyChanged) {
notificationService.sendUrgencyNotification(newUrgency, volumeMl.toInt())
}
// Phase D (GUARDIAN-01 · 2026-08-03) — ClinicalEventBus emit.
runCatching {
com.medithings.vesiscan.telemetry.ClinicalEventBus.emit(
com.medithings.vesiscan.telemetry.BvEvent(
ts = System.currentTimeMillis(),
sessionId = com.medithings.vesiscan.telemetry.TelemetryHash.sessionId(
bleManager.connectedDeviceName.value.ifEmpty { "unknown" }
),
severity = when (newUrgency) {
com.medithings.vesiscan.models.UrgencyLevel.URGENT ->
com.medithings.vesiscan.telemetry.Severity.CRITICAL
com.medithings.vesiscan.models.UrgencyLevel.WARNING ->
com.medithings.vesiscan.telemetry.Severity.WARN
else -> com.medithings.vesiscan.telemetry.Severity.INFO
},
volumeMl = volumeMl.toInt(),
urgency = newUrgency,
bvMethod = com.medithings.vesiscan.managers.GreenZoneConstants.bvMethod.name,
isTransition = urgencyChanged,
prevUrgency = previousUrgency.takeIf { urgencyChanged },
)
)
}
}
fun resetLevel() {
val record = MeasurementRecord(
eventType = MeasurementEventType.CATHETERIZATION,
level = bladderLevel.currentLevel,
estimatedVolume = estimatedVolume,
urgencyLevel = bladderLevel.urgency,
fillPercentage = fillPercentage
)
historyStore.addRecord(record)
val newLevel = bladderLevel.copy()
newLevel.reset()
bladderLevel = newLevel
storage.bladderLevel = 0
notificationService.cancelAll()
reminderService.reschedule()
if (catheterCount.intValue > 0) {
catheterCount.intValue--
storage.catheterCount = catheterCount.intValue
}
}
fun showHistory() {
currentScreen = AppScreen.MEASUREMENT_HISTORY
}
fun backToMonitoring() {
currentScreen = AppScreen.PIEZO_MONITORING
}
fun addCatheterCount(amount: Int = 1) {
catheterCount.intValue += amount
storage.catheterCount = catheterCount.intValue
}
fun updateCatheterizationThreshold(threshold: Int) {
bladderLevel = bladderLevel.copy(catheterizationThreshold = threshold)
storage.catheterizationLevel = threshold
}
fun savePiezoSettings() {
storage.piezoSettings = piezoSettings
}
val estimatedVolume: Int
get() {
if (BladderLevel.MAX_LEVEL <= 0) return 0
return (bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * piezoSettings.maxVolume).toInt()
}
val fillPercentage: Double
get() {
if (BladderLevel.MAX_LEVEL <= 0) return 0.0
return bladderLevel.currentLevel.toDouble() / BladderLevel.MAX_LEVEL * 100.0
}
}