c7f9cc976e
화면 안 뒤로가기 화살표가 CLINICAL_HOME 으로 보내고 있었다. 이 화면의 진입점은 **시작 화면(HOME)** 의 "병원 임상 측정" 버튼이라, 누른 적도 없는 다른 모드로 빠져나갔다. 둘은 목적도 저장 경로도 다르다(임상 R&D = VesiScan_Sessions, 병원 임상 = VesiScan_Hospital). 하드웨어 뒤로가기는 else 로 흘러 이미 HOME 이었지만, 헷갈리기 쉬운 자리라 BackHandler 에도 명시했다. 측정 중 나가면 루프가 취소되고 finally 가 그때까지의 결과를 매니페스트에 남긴다 — 진행 표시에서 '일부'로 보인다. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
227 lines
12 KiB
Kotlin
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 }
|
|
)
|
|
}
|
|
}
|
|
} |