feat(guardian): Rev2 · Bus 확장 + AlarmAggregator + NotificationSubscriber
GUARDIAN-01 §Rev2 개선 4건 (파트너 앱 개발 지원):
1. BleManager · Battery/BleConnection emit 지점 추가
- emitBatteryEvent(pct): 임계 bucket 전이 (CRITICAL<5, LOW<20, OK≥20)
- emitConnectionEvent(state, reason): CONNECTED / DISCONNECTED / FAILED
- CONNECTED · STATE_DISCONNECTED · GATT_ERROR · 좀비 watchdog 4 경로 wire
2. AlarmAggregator (신규 · telemetry/) — subscriber 참조 구현
- Bus 구독 · BvEvent/PostureEvent/GaitEvent/BleConn/Battery → AlarmEvent 재발행
- 재귀 방지 (filterNot { it is AlarmEvent })
- Category 매핑: BV_URGENT/BV_WARN/POSTURE_TRANSITION/GAIT_STARTED/STOPPED/
BLE_LOST/BATTERY_CRITICAL/BATTERY_LOW
- Guardian 앱 로직 단순화 (AlarmEvent 하나만 구독하면 됨)
3. Dev-mode overlay 확장 (PiezoMonitoringView · RSSI 옆)
- ClinicalEventBus 최근 5개 이벤트 rolling 표시
- Type(BV/PST/GAI/BLE/BAT/ALM) + severity color + 요약
- LaunchedEffect + mutableStateListOf · isDevMode gate
4. NotificationSubscriber (신규 · telemetry/) — in-app 소비 참조 구현
- AlarmEvent 구독 · Android push notification 발행
- BATTERY_CRITICAL/LOW → device_status 채널 (신규)
- BLE_LOST → 30초 debounce · CONNECTED 시 reset
- BV_URGENT/WARN → AppState.updateLevelFromMeasurement 직접 호출과 중복 방지 · NO-OP
- MainActivity.onCreate 에서 start(this) 1회 호출
NotificationService 확장:
- device_status 채널 신규 (배터리·BLE 알람용)
- sendBatteryNotification(level, isCritical) · bucket dedup
- sendBleLostNotification(reason) · resetBleDebounce()
의도: Guardian 앱 (동업자 · Supabase 경유) 이 in-app subscriber 와 동일 패턴으로
소비하도록 참조. 다음 단계는 GUARDIAN-02 파트너용 스키마 문서 (cloud-mvp).
This commit is contained in:
@@ -20,9 +20,17 @@ class NotificationService private constructor(private val context: Context) {
|
||||
|
||||
private const val CHANNEL_ID = "bladder_urgency"
|
||||
private const val CHANNEL_NAME = "Bladder Monitoring"
|
||||
private const val DEVICE_CHANNEL_ID = "device_status"
|
||||
private const val BATTERY_NOTIF_ID = 1001
|
||||
private const val BLE_NOTIF_ID = 1002
|
||||
/** BLE 알람 최소 간격 (재연결/끊김 반복 시 스팸 방지) · ms */
|
||||
private const val BLE_DEBOUNCE_MS = 30_000L
|
||||
}
|
||||
|
||||
private var lastNotifiedUrgency: String? = null
|
||||
// GUARDIAN-01 Phase D · 신규 채널 dedup key (battery bucket, BLE state).
|
||||
@Volatile private var lastBatteryBucket: Int = -1
|
||||
@Volatile private var lastBleAlertMs: Long = 0L
|
||||
|
||||
init {
|
||||
createNotificationChannel()
|
||||
@@ -30,16 +38,20 @@ class NotificationService private constructor(private val context: Context) {
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val bladder = NotificationChannel(
|
||||
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH,
|
||||
).apply {
|
||||
description = "Bladder level urgency notifications"
|
||||
enableVibration(true)
|
||||
}
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
val device = NotificationChannel(
|
||||
DEVICE_CHANNEL_ID, "Device Status", NotificationManager.IMPORTANCE_DEFAULT,
|
||||
).apply {
|
||||
description = "Battery low / BLE disconnect notifications"
|
||||
}
|
||||
nm.createNotificationChannel(bladder)
|
||||
nm.createNotificationChannel(device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +82,50 @@ class NotificationService private constructor(private val context: Context) {
|
||||
} catch (_: SecurityException) { }
|
||||
}
|
||||
|
||||
/** 배터리 임계 알림 · bucket 전이 시 1회만. isCritical=true → CRITICAL, false → LOW. */
|
||||
fun sendBatteryNotification(level: Int, isCritical: Boolean) {
|
||||
val bucket = if (isCritical) 0 else 1
|
||||
if (bucket == lastBatteryBucket) return
|
||||
lastBatteryBucket = bucket
|
||||
val title = if (isCritical) "Battery Critical" else "Battery Low"
|
||||
val body = "Device battery at $level%. Please charge soon."
|
||||
val priority = if (isCritical) NotificationCompat.PRIORITY_HIGH else NotificationCompat.PRIORITY_DEFAULT
|
||||
val notif = NotificationCompat.Builder(context, DEVICE_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setPriority(priority)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
try { NotificationManagerCompat.from(context).notify(BATTERY_NOTIF_ID, notif) }
|
||||
catch (_: SecurityException) { }
|
||||
}
|
||||
|
||||
/** BLE 연결 해제 알림 · 30초 내 중복 억제. */
|
||||
fun sendBleLostNotification(reason: String?) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastBleAlertMs < BLE_DEBOUNCE_MS) return
|
||||
lastBleAlertMs = now
|
||||
val notif = NotificationCompat.Builder(context, DEVICE_CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle("Device Disconnected")
|
||||
.setContentText(if (reason != null) "BLE connection lost · $reason" else "BLE connection lost")
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
try { NotificationManagerCompat.from(context).notify(BLE_NOTIF_ID, notif) }
|
||||
catch (_: SecurityException) { }
|
||||
}
|
||||
|
||||
/** BLE 재연결 성공 시 debounce 리셋. */
|
||||
fun resetBleDebounce() {
|
||||
lastBleAlertMs = 0L
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
lastNotifiedUrgency = null
|
||||
lastBatteryBucket = -1
|
||||
lastBleAlertMs = 0L
|
||||
NotificationManagerCompat.from(context).cancelAll()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user