Files
VesiscanClinicalAndroid/app/src/main/java/com/medithings/vesiscan/MainActivity.kt
T
dw.jang c7f9cc976e fix(clinical): 병원 임상 모드 뒤로가기가 임상 R&D 모드로 튀던 것
화면 안 뒤로가기 화살표가 CLINICAL_HOME 으로 보내고 있었다. 이 화면의 진입점은
**시작 화면(HOME)** 의 "병원 임상 측정" 버튼이라, 누른 적도 없는 다른 모드로
빠져나갔다. 둘은 목적도 저장 경로도 다르다(임상 R&D = VesiScan_Sessions,
병원 임상 = VesiScan_Hospital).

하드웨어 뒤로가기는 else 로 흘러 이미 HOME 이었지만, 헷갈리기 쉬운 자리라
BackHandler 에도 명시했다. 측정 중 나가면 루프가 취소되고 finally 가 그때까지의
결과를 매니페스트에 남긴다 — 진행 표시에서 '일부'로 보인다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:41:43 +09:00

227 lines
12 KiB
Kotlin

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 }
)
}
}
}