package com.medithings.vesiscan import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import app.rive.runtime.kotlin.core.Rive import com.medithings.vesiscan.services.KeychainService import com.medithings.vesiscan.services.NotificationService import com.medithings.vesiscan.R import com.medithings.vesiscan.ui.theme.Medilightv2androidTheme import com.medithings.vesiscan.ui.views.connection.DeviceScanView import com.medithings.vesiscan.ui.views.history.MeasurementHistoryView import com.medithings.vesiscan.ui.views.home.HomeView import com.medithings.vesiscan.ui.views.monitoring.PlacementGuideView import com.medithings.vesiscan.ui.views.monitoring.PiezoMonitoringView import com.medithings.vesiscan.ui.views.monitoring.VoidingDiaryView import com.medithings.vesiscan.ui.views.onboarding.OnboardingView import com.medithings.vesiscan.ui.views.personalization.PiezoPersonalizationView import com.medithings.vesiscan.ui.views.pin.PinMode import com.medithings.vesiscan.ui.views.pin.PinView import com.medithings.vesiscan.ui.views.registration.RegistrationView import com.medithings.vesiscan.ui.views.consent.ConsentView import com.medithings.vesiscan.ui.views.reminder.ReminderSettingsView import com.medithings.vesiscan.ui.views.sensorselect.SensorSelectView import com.medithings.vesiscan.ui.views.clinical.ClinicalHomeView import com.medithings.vesiscan.ui.views.clinical.ClinicalLiveView class MainActivity : ComponentActivity() { /** 사용자가 선택한 앱 언어 적용 — Activity 생성 전에 Locale wrap. */ override fun attachBaseContext(newBase: android.content.Context) { super.attachBaseContext(com.medithings.vesiscan.services.LocalePref.wrap(newBase)) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialize services Rive.init(this) KeychainService.init(this) NotificationService.getInstance(this) // GUARDIAN-01 Phase D · Bus 인프라 부팅. // 1) AlarmAggregator: BV/Posture/Gait/BLE/Battery → AlarmEvent 재발행 // 2) NotificationSubscriber: AlarmEvent → Android push (battery/BLE 신규) com.medithings.vesiscan.telemetry.AlarmAggregator.start() com.medithings.vesiscan.telemetry.NotificationSubscriber.start(this) // 2026-08-04: LOG_STORAGE_ANALYSIS §C fix — 앱 시작 시 오래된 로그 자동 정리. // 실패해도 필수기능 영향 없음 (silent · UC-05). try { val app = applicationContext com.medithings.vesiscan.services.MeasurementLogService.getInstance(app) .purgeOldLogs(keepDays = 365) com.medithings.vesiscan.services.BleDebugLogger.getInstance(app) .purgeOldLogs(keepDays = 7) com.medithings.vesiscan.services.AdcCsvLogger.purgeOldSessions(keepDays = 30) com.medithings.vesiscan.services.ClinicalSessionStore.purgeOldSessions(keepDays = 90) } catch (_: Exception) { } enableEdgeToEdge() window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) setContent { Medilightv2androidTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> MedilightApp(modifier = Modifier.padding(innerPadding)) } } } } /** * 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 */ } // BLE 로그 파일 마무리. 줄마다 flush 하므로 이 호출을 놓쳐도(강제 종료 등) // 그 시점까지의 내용은 이미 디스크에 있다 — 여기서는 마무리 표시만 남긴다. try { com.medithings.vesiscan.services.BleDebugLogger.getInstance(applicationContext).close() } catch (_: Exception) {} super.onDestroy() } } @Composable fun MedilightApp(modifier: Modifier = Modifier) { val context = LocalContext.current val appState = remember { AppState(context) } // HOME에서 뒤로가기 → 앱 종료 확인 var showExitDialog by remember { mutableStateOf(false) } BackHandler(enabled = appState.currentScreen == AppScreen.HOME) { showExitDialog = true } if (showExitDialog) { androidx.compose.material3.AlertDialog( onDismissRequest = { showExitDialog = false }, title = { androidx.compose.material3.Text(stringResource(R.string.exit_app_title)) }, text = { androidx.compose.material3.Text(stringResource(R.string.exit_app_message)) }, confirmButton = { androidx.compose.material3.TextButton(onClick = { (context as? android.app.Activity)?.finish() }) { androidx.compose.material3.Text(stringResource(R.string.exit_app_confirm), color = Color.Red) } }, dismissButton = { androidx.compose.material3.TextButton(onClick = { showExitDialog = false }) { androidx.compose.material3.Text(stringResource(R.string.cancel)) } } ) } // 다른 화면에서 뒤로가기 = 바로 전 화면 BackHandler(enabled = appState.currentScreen != AppScreen.HOME && appState.currentScreen != AppScreen.PIN_ENTRY) { when (appState.currentScreen) { // 메인 플로우: Home → DeviceScan → Personalization → Placement → Monitoring // DeviceScan Back: 임상 흐름이면 ClinicalHome으로, 아니면 HOME AppScreen.DEVICE_SCAN -> appState.currentScreen = // 부른 화면이 복귀처를 지정했으면 그리로 (병원 임상 모드 등). appState.consumeConnectReturn() ?: if (com.medithings.vesiscan.services.ClinicalSessionStore.inClinicalFlow) AppScreen.CLINICAL_HOME else AppScreen.HOME AppScreen.PIEZO_PERSONALIZATION -> appState.currentScreen = AppScreen.DEVICE_SCAN // 임상 모드 측정 중이면 endMeasurement → ClinicalHome 복귀 AppScreen.PLACEMENT_GUIDE -> if (com.medithings.vesiscan.services.ClinicalSessionStore.currentSession != null) { com.medithings.vesiscan.services.ClinicalSessionStore.endMeasurement(context) appState.currentScreen = AppScreen.CLINICAL_HOME } else appState.backFromPlacementGuide() AppScreen.PIEZO_MONITORING -> if (com.medithings.vesiscan.services.ClinicalSessionStore.currentSession != null) { com.medithings.vesiscan.services.ClinicalSessionStore.endMeasurement(context) appState.currentScreen = AppScreen.CLINICAL_HOME } else appState.currentScreen = AppScreen.PLACEMENT_GUIDE AppScreen.CLINICAL_HOME -> { com.medithings.vesiscan.services.ClinicalSessionStore.inClinicalFlow = false appState.currentScreen = AppScreen.HOME } AppScreen.CLINICAL_LIVE -> { // 측정 폴더 finalize 후 ClinicalHome 복귀 com.medithings.vesiscan.services.ClinicalSessionStore.endMeasurement(context) appState.currentScreen = AppScreen.CLINICAL_HOME } // 모니터링 하위 화면 → 모니터링 AppScreen.MEASUREMENT_HISTORY -> appState.currentScreen = AppScreen.PIEZO_MONITORING AppScreen.VOIDING_DIARY -> appState.currentScreen = AppScreen.PIEZO_MONITORING AppScreen.REMINDER_SETTINGS -> appState.currentScreen = AppScreen.PIEZO_MONITORING AppScreen.FIRMWARE_UPDATE -> appState.currentScreen = AppScreen.HOME // 정렬은 병원 임상 모드에서 들어온다 — 돌아갈 곳도 거기다. AppScreen.ANCHOR_ALIGN -> appState.currentScreen = AppScreen.HOSPITAL_MODE // 병원 임상 모드의 진입점은 시작 화면이다. else 로 흘려도 HOME 이지만 // 임상 R&D 모드(CLINICAL_HOME)와 헷갈리기 쉬운 자리라 명시해 둔다. // 측정 중 나가면 루프가 취소되고 finally 가 그때까지의 결과를 매니페스트에 // 남긴다(진행 표시에서 '일부'로 보인다). AppScreen.HOSPITAL_MODE -> appState.currentScreen = AppScreen.HOME // 기타 AppScreen.SENSOR_SELECT -> appState.currentScreen = AppScreen.HOME // 온보딩/등록 (뒤로가기 막지 않되 Home으로) else -> appState.currentScreen = AppScreen.HOME } } Crossfade( targetState = appState.currentScreen, animationSpec = tween(300), modifier = modifier, label = "screen" ) { screen -> when (screen) { AppScreen.ONBOARDING -> OnboardingView(appState) AppScreen.CONSENT -> ConsentView(appState) AppScreen.REGISTRATION -> RegistrationView(appState) AppScreen.HOME -> HomeView(appState) AppScreen.SENSOR_SELECT -> SensorSelectView(appState) AppScreen.DEVICE_SCAN -> DeviceScanView(appState) AppScreen.PIEZO_PERSONALIZATION -> PiezoPersonalizationView(appState) AppScreen.PLACEMENT_GUIDE -> PlacementGuideView(appState) AppScreen.PIN_SETUP -> PinView(appState, PinMode.SETUP, context) AppScreen.PIN_ENTRY -> PinView(appState, PinMode.ENTER, context) AppScreen.PIEZO_MONITORING -> PiezoMonitoringView(appState) AppScreen.MEASUREMENT_HISTORY -> MeasurementHistoryView(appState) AppScreen.VOIDING_DIARY -> VoidingDiaryView(onBack = { appState.currentScreen = AppScreen.HOME }) AppScreen.REMINDER_SETTINGS -> ReminderSettingsView( onBack = { appState.backToMonitoring() } ) AppScreen.CLINICAL_HOME -> ClinicalHomeView(appState) AppScreen.HOSPITAL_MODE -> com.medithings.vesiscan.ui.views.clinical.HospitalModeView(appState) AppScreen.ANCHOR_ALIGN -> com.medithings.vesiscan.ui.views.clinical.AnchorAlignView(appState) AppScreen.CLINICAL_LIVE -> ClinicalLiveView(appState) AppScreen.FIRMWARE_UPDATE -> com.medithings.vesiscan.firmware.ui.FirmwareUpdateScreen( onBack = { appState.currentScreen = AppScreen.HOME } ) } } }