471 lines
21 KiB
Plaintext
471 lines
21 KiB
Plaintext
@startuml WalkGuide_StateMachine
|
||
title State Machine Diagram — WalkGuide System (Full Architecture)
|
||
|
||
skinparam state {
|
||
BackgroundColor WhiteSmoke
|
||
BorderColor #5B6AB0
|
||
FontColor Black
|
||
ArrowColor #5B6AB0
|
||
StartColor Black
|
||
EndColor Black
|
||
}
|
||
skinparam note {
|
||
BackgroundColor LightYellow
|
||
BorderColor #999
|
||
}
|
||
|
||
' ============================================================
|
||
' TOP LEVEL: APP LAUNCH
|
||
' ============================================================
|
||
[*] --> AppLaunched : App dibuka
|
||
|
||
state AppLaunched {
|
||
[*] --> CheckingServerUrl
|
||
|
||
CheckingServerUrl : Cek SharedPreferences\nserver URL tersimpan?
|
||
|
||
CheckingServerUrl --> ServerConnectScreen : [Tidak ada URL]
|
||
CheckingServerUrl --> CheckingToken : [URL tersedia]
|
||
|
||
ServerConnectScreen : Input server URL\n(http://202.46.28.160:8080)
|
||
ServerConnectScreen --> PingingServer : Submit URL
|
||
|
||
PingingServer : GET /api/v1/auth/ping\n(cek koneksi ke backend)
|
||
PingingServer --> ServerConnectScreen : [Ping Gagal / Timeout]
|
||
PingingServer --> CheckingToken : [Ping OK → simpan URL ke SharedPreferences]
|
||
|
||
CheckingToken : Cek flutter_secure_storage\naccessToken ada?
|
||
CheckingToken --> Unauthenticated : [Token Tidak Ada]
|
||
CheckingToken --> ValidatingToken : [Token Ada]
|
||
|
||
ValidatingToken : Verifikasi JWT\n(decode expiry lokal)
|
||
ValidatingToken --> Unauthenticated : [Token Expired / Invalid]
|
||
ValidatingToken --> Authenticated : [Token Valid → decode role]
|
||
}
|
||
|
||
' ============================================================
|
||
' UNAUTHENTICATED
|
||
' ============================================================
|
||
state Unauthenticated {
|
||
[*] --> LoginScreen
|
||
|
||
LoginScreen : Input email & password\n+ tombol ke RegisterScreen
|
||
|
||
LoginScreen --> RegisterScreen : Tap "Register"
|
||
RegisterScreen --> LoginScreen : Tap "Back to Login"
|
||
|
||
RegisterScreen : Pilih role (ROLE_GUARDIAN / ROLE_USER)\nIsi email, password, displayName
|
||
|
||
LoginScreen --> ValidatingCredentials : Submit login
|
||
RegisterScreen --> RegisteringUser : Submit register
|
||
|
||
ValidatingCredentials : POST /api/v1/auth/login\n{email, password}
|
||
ValidatingCredentials --> LoginScreen : [401 Unauthorized → tampil error]
|
||
ValidatingCredentials --> StoringAuthData : [200 OK + JWT]
|
||
|
||
RegisteringUser : POST /api/v1/auth/register\n{email, password, role, displayName}
|
||
RegisteringUser --> LoginScreen : [400 / Email sudah terdaftar]
|
||
RegisteringUser --> StoringAuthData : [201 Created + JWT]
|
||
|
||
StoringAuthData : Simpan accessToken + refreshToken\nke flutter_secure_storage\nUpdate FCM token → PUT /auth/fcm-token\nWebSocketService.connect(accessToken)
|
||
StoringAuthData --> DecodingRole : JWT tersimpan
|
||
|
||
DecodingRole : Ekstrak role dari JWT claims
|
||
DecodingRole --> [*]
|
||
}
|
||
|
||
note on link
|
||
Login Sukses
|
||
200 OK + JWT
|
||
(access + refresh)
|
||
end note
|
||
|
||
Unauthenticated --> Authenticated : [Login / Register Sukses → role decoded]
|
||
|
||
' ============================================================
|
||
' AUTHENTICATED (role router)
|
||
' ============================================================
|
||
state Authenticated {
|
||
[*] --> CheckingPairing
|
||
|
||
CheckingPairing : GET /shared/pairing/status\ncek status pairing user ini
|
||
|
||
CheckingPairing --> UserShell : [Role = ROLE_USER]
|
||
CheckingPairing --> GuardianShell : [Role = ROLE_GUARDIAN]
|
||
}
|
||
|
||
' ============================================================
|
||
' USER SHELL
|
||
' ============================================================
|
||
state UserShell {
|
||
|
||
[*] --> UserIdle
|
||
|
||
UserIdle : Bottom nav 6 tabs aktif\nSTT always-listening di background\nWebSocket subscribe /queue/notif/{userId}
|
||
note right of UserIdle
|
||
Voice commands aktif:\n"Start Walkguide", "Open SOS",\n"Call Guardian", "Where Am I", dll
|
||
end note
|
||
|
||
' --- PAIRING CHECK ---
|
||
UserIdle --> PairingScreen_User : [Belum paired → TTS peringatan]
|
||
|
||
state PairingScreen_User {
|
||
[*] --> WaitingPairingInvite
|
||
WaitingPairingInvite : GET /shared/pairing/status\nTampil uniqueUserId 12-char\n"Share this ID with your Guardian"
|
||
WaitingPairingInvite --> ReceivingInvite : FCM: PAIRING_INVITE diterima
|
||
ReceivingInvite --> WaitingPairingInvite : Tap "Reject" → POST /pairing/respond {accept:false}
|
||
ReceivingInvite --> PairingAccepted : Tap "Accept" → POST /pairing/respond {accept:true}
|
||
PairingAccepted : Backend seed 14 VoiceCommands\n+ 5 Shortcuts + AiConfig defaults\nFCM ke Guardian: "Pairing accepted"\nTTS: "Pairing successful. Guardian connected."
|
||
PairingAccepted --> [*]
|
||
}
|
||
|
||
PairingScreen_User --> UserIdle : Pairing berhasil / lewati
|
||
|
||
' --- WALKGUIDE ---
|
||
UserIdle --> WalkGuideActive : Tap "Start Walk Guide"\nATAU voice "Start Walkguide"\nATAU Volume Down button
|
||
|
||
state WalkGuideActive {
|
||
[*] --> InitializingCamera
|
||
|
||
InitializingCamera : camera.startImageStream()\nRequest permissions (kamera, lokasi)\nPOST /user/walkguide/start → ActivityLog: WALKGUIDE_START
|
||
InitializingCamera --> [*] : [Permission Denied → kembali ke UserIdle]
|
||
|
||
InitializingCamera --> DetectionLoop : Kamera siap\nTTS: "3... 2... 1... WalkGuide started."
|
||
|
||
state DetectionLoop {
|
||
[*] --> CapturingFrame
|
||
|
||
CapturingFrame : Terima CameraImage (YUV420)\nThrottle: maxInferenceFps (default 5fps)
|
||
CapturingFrame --> ProcessingFrame : [YoloDetector tidak sedang running → kirim frame]
|
||
CapturingFrame --> CapturingFrame : [YoloDetector sedang running → skip frame ini]
|
||
|
||
ProcessingFrame : YUV420→RGB→Resize 640×640\nNormalize 0.0-1.0\ninterpreter.run() [Isolate terpisah]\nPost-process + NMS\nFilter by confidenceThreshold
|
||
|
||
ProcessingFrame --> EvaluatingResult : Inference selesai
|
||
|
||
EvaluatingResult : ObstacleAnalyzer.prioritize(results)\nTentukan direction (LEFT/CENTER/RIGHT)\nEstimasi distance (Very Close/Close/Medium/Far)
|
||
|
||
EvaluatingResult --> ObstacleDetected : [Confidence ≥ threshold & ada deteksi]
|
||
EvaluatingResult --> NoObstacleDetected : [Confidence < threshold / tidak ada deteksi]
|
||
|
||
ObstacleDetected : Build TTS: "Caution! {label} {direction}. {distance}. Please stop."\nttsService.speakImmediate()\nhapticService.obstacleVeryClose()\nDebounce 3 detik (jangan log yang sama terus)\nPOST /user/obstacle {label, confidence, direction, dist, lat, lng}
|
||
|
||
NoObstacleDetected : Lanjut deteksi
|
||
|
||
ObstacleDetected --> CapturingFrame : Feedback selesai
|
||
NoObstacleDetected --> CapturingFrame : Lanjut deteksi
|
||
|
||
' Location update loop (parallel, setiap 5 detik)
|
||
CapturingFrame --> SendingLocation : [Setiap 5 detik]
|
||
SendingLocation : geolocator.getCurrentPosition()\nPOST /user/location {lat, lng, accuracy, speed, heading}\nBackend: simpan LocationHistory\nWebSocket broadcast → Guardian map\nGeofenceService.checkGeofence()
|
||
SendingLocation --> CapturingFrame : Location terkirim
|
||
}
|
||
|
||
DetectionLoop --> SavingSession : Tap "Stop Walk Guide"\nATAU voice "Stop Walkguide"
|
||
|
||
SavingSession : camera.stopImageStream()\nPOST /user/walkguide/stop → ActivityLog: WALKGUIDE_STOP\nLocation interval → 30 detik
|
||
SavingSession --> [*]
|
||
}
|
||
|
||
WalkGuideActive --> UserIdle : Sesi WalkGuide selesai
|
||
|
||
' --- SOS ---
|
||
UserIdle --> SosFlow : Tap SOS tab\nATAU voice "Send SOS"\nATAU hardware shortcut
|
||
|
||
state SosFlow {
|
||
[*] --> SosConfirm
|
||
SosConfirm : SosScreen: tombol SOS merah besar\nTTS: "SOS screen. Press the big red button\nor say 'Send SOS' to alert your Guardian."
|
||
SosConfirm --> TriggeringSos : Tap SOS button / voice "Send SOS"
|
||
TriggeringSos : Get GPS position\nPOST /user/sos {triggerType, lat, lng}\nBackend: save SosEvent TRIGGERED\nFCM HIGH PRIORITY ke Guardian: "🚨 SOS ALERT!"\nWebSocket → /queue/sos/{guardianId}\nTTS: "SOS sent. Guardian has been alerted."\nhapticService.sosTriggered()
|
||
TriggeringSos --> WaitingAcknowledge : SOS terkirim
|
||
WaitingAcknowledge : Tunggu respons Guardian\nTampil status "Waiting for Guardian..."
|
||
WaitingAcknowledge --> SosAcknowledged : FCM: Guardian acknowledge SOS\nTTS: "Your Guardian has acknowledged.\nHelp is coming."
|
||
SosAcknowledged --> [*]
|
||
}
|
||
|
||
SosFlow --> UserIdle : Kembali ke home
|
||
|
||
' --- NOTIFICATIONS ---
|
||
UserIdle --> NotificationFlow : Tap Notifications tab\nATAU voice "Open Notifications"\nATAU FCM/WebSocket masuk
|
||
|
||
state NotificationFlow {
|
||
[*] --> ViewingNotifications
|
||
ViewingNotifications : GET /user/notifications\nGET /user/notifications/unread-count\nTampil list: TEXT / VOICE_NOTE\nBadge unread count\nTTS: "Notifications. You have {N} unread messages."
|
||
ViewingNotifications --> ReadingAllTts : Tap "Read All" / voice "Read All My Notifications"
|
||
ViewingNotifications --> ReadingOneTts : Tap satu notifikasi
|
||
ReadingAllTts : Iterate unread list:\n- TEXT: TTS "From Guardian on {date}: {content}"\n- VOICE_NOTE: just_audio.play(voiceNoteUrl)\nPUT /user/notifications/{id}/read setelah selesai
|
||
ReadingOneTts : TTS / play satu notifikasi\nPUT /user/notifications/{id}/read
|
||
ReadingAllTts --> ViewingNotifications : Semua notif dibaca
|
||
ReadingOneTts --> ViewingNotifications : Selesai
|
||
ViewingNotifications --> MarkingAllRead : Tap "Mark All Read"\nPUT /user/notifications/mark-all-read
|
||
MarkingAllRead --> ViewingNotifications : Done
|
||
}
|
||
|
||
NotificationFlow --> UserIdle : Kembali
|
||
|
||
' --- NAVIGATION MODE ---
|
||
UserIdle --> NavigationModeActive : Tap Navigate tab\nATAU voice "Open Navigation"
|
||
|
||
state NavigationModeActive {
|
||
[*] --> NavigationIdle
|
||
NavigationIdle : FlutterMap (OpenStreetMap tiles)\nTTS: "Navigation mode. Say a destination."
|
||
NavigationIdle --> SearchingDestination : Tap search bar / voice "Navigate to {place}"
|
||
SearchingDestination : OSM Nominatim API\nGET nominatim.openstreetmap.org/search?q=...
|
||
SearchingDestination --> NavigatingRoute : Lokasi ditemukan → OSRM routing
|
||
NavigatingRoute : OSRM turn-by-turn route\nTTS setiap instruksi: "In 50 meters, turn right"\ngeolocator: GPS real-time\nPOST /user/location setiap 5 detik
|
||
NavigatingRoute --> NavigationIdle : Sampai tujuan / Stop
|
||
}
|
||
|
||
NavigationModeActive --> UserIdle : Kembali
|
||
|
||
' --- CALL ---
|
||
UserIdle --> OutgoingCallFlow : Tap "Call Guardian"\nATAU voice "Call Guardian"\nATAU Volume Up button
|
||
|
||
state OutgoingCallFlow {
|
||
[*] --> RequestingAgoraToken
|
||
RequestingAgoraToken : POST /shared/call/token\nGenerate Agora RTC token\nPOST /shared/call/notify → FCM ke Guardian "Incoming Call"
|
||
RequestingAgoraToken --> CallingGuardian : Token dapat, join Agora channel
|
||
CallingGuardian : CallScreen: "Calling Guardian..." + animasi\nAgoraRtcEngine.joinChannel()
|
||
CallingGuardian --> InCall : Guardian pick up → CallConnected
|
||
CallingGuardian --> UserIdle : Guardian decline / timeout 30 detik
|
||
InCall : Timer durasi call\nTombol: Mute, Speaker, End Call\nTTS: "Connected to Guardian"
|
||
InCall --> UserIdle : Tap End Call
|
||
}
|
||
|
||
UserIdle --> IncomingCallHandled : FCM INCOMING_CALL diterima dari Guardian
|
||
|
||
state IncomingCallHandled {
|
||
[*] --> ShowingIncomingCall
|
||
ShowingIncomingCall : IncomingCallScreen\nTTS: "Incoming call from Guardian"\nHaptic vibration\nTombol Accept (hijau) + Decline (merah)\nAuto-answer setelah 30 detik
|
||
ShowingIncomingCall --> InCallSession : Accept
|
||
ShowingIncomingCall --> UserIdle : Decline
|
||
InCallSession : Agora RTC connected\nTimer + Mute + Speaker + End
|
||
InCallSession --> UserIdle : End call
|
||
}
|
||
|
||
' --- SETTINGS ---
|
||
UserIdle --> UserSettingsFlow : Tap Settings tab\nATAU voice "Open Settings"
|
||
|
||
state UserSettingsFlow {
|
||
[*] --> ViewingSettings
|
||
ViewingSettings : TTS Settings (read-only pitch/speed)\nPairing status link\nManual/Instructions link\nNo-Guardian Warning toggle\nAccount info + Logout
|
||
ViewingSettings --> UpdatingTtsLanguage : Toggle ID/EN → PUT /user/settings
|
||
ViewingSettings --> ViewingManual : Tap "Instructions"
|
||
ViewingSettings --> LoggingOut : Tap Logout
|
||
UpdatingTtsLanguage --> ViewingSettings : Saved
|
||
ViewingManual : ManualScreen\nSemua voice commands + shortcuts\nTombol "Hear Instructions"\nTTS setiap instruksi 1-per-1
|
||
ViewingManual --> ViewingSettings : Back
|
||
LoggingOut : POST /auth/logout {refreshToken}\nSecureStorage.clearAll()\nWebSocketService.disconnect()\nGoRouter → /login
|
||
LoggingOut --> [*]
|
||
}
|
||
|
||
UserSettingsFlow --> UserIdle : Kembali
|
||
UserSettingsFlow --> [*] : Logout
|
||
|
||
' --- ACTIVITY LOG ---
|
||
UserIdle --> ActivityLogFlow : Tap Activity tab\nATAU voice "Open Activity Log"
|
||
|
||
state ActivityLogFlow {
|
||
[*] --> ViewingActivityLog
|
||
ViewingActivityLog : GET /user/activity-logs (paginated)\nInfinite scroll\nFilter by type/date\nTTS: "Activity log. {N} activities today."
|
||
}
|
||
|
||
ActivityLogFlow --> UserIdle : Kembali
|
||
}
|
||
|
||
' ============================================================
|
||
' GUARDIAN SHELL
|
||
' ============================================================
|
||
state GuardianShell {
|
||
|
||
[*] --> GuardianIdle
|
||
|
||
GuardianIdle : GuardianDashboardScreen HOME\nCard: User status (online/offline, battery)\nCard: Last location (Nominatim reverse)\nCard: Unread SOS (badge merah)\nCard: Recent 5 activity logs\nQuick actions: Send Message, Call User, View Map\nWebSocket subscribe:\n /topic/location/{pairedUserId}\n /queue/sos/{guardianId}
|
||
|
||
' --- PAIRING (Guardian side) ---
|
||
GuardianIdle --> GuardianPairingFlow : Tap "Manage Pairing"\nATAU belum punya paired user
|
||
|
||
state GuardianPairingFlow {
|
||
[*] --> EnteringUserId
|
||
EnteringUserId : Input uniqueUserId 12-char\nmilik paired User
|
||
EnteringUserId --> SendingInvite : Tap "Send Invite"
|
||
SendingInvite : POST /shared/pairing/invite {uniqueUserId}\nBackend: buat PairingRelation PENDING\nFCM ke User: "Pairing Request"
|
||
SendingInvite --> WaitingUserResponse : Invite terkirim
|
||
WaitingUserResponse : Tunggu User accept/reject
|
||
WaitingUserResponse --> PairingSuccess : FCM: User accepted\nBackend seed VoiceCommands + Shortcuts + AiConfig
|
||
WaitingUserResponse --> EnteringUserId : FCM: User rejected
|
||
PairingSuccess : TTS: "Pairing successful. User is now connected."\nNavigate → GuardianIdle
|
||
PairingSuccess --> [*]
|
||
}
|
||
|
||
GuardianPairingFlow --> GuardianIdle : Selesai
|
||
|
||
' --- MAP / LIVE TRACKING ---
|
||
GuardianIdle --> GuardianMapView : Tap "View Map"\nATAU quick action map
|
||
|
||
state GuardianMapView {
|
||
[*] --> ViewingLiveMap
|
||
ViewingLiveMap : FlutterMap + live location marker\nWebSocket /topic/location/{userId}\nGeofence circle overlay (jika enabled)\nRoute history polyline (location_history)
|
||
ViewingLiveMap --> ViewingLiveMap : [WebSocket update → marker pindah]
|
||
}
|
||
|
||
GuardianMapView --> GuardianIdle : Kembali
|
||
|
||
' --- ACTIVITY LOGS ---
|
||
GuardianIdle --> GuardianActivityLog : Tap "View Activity Logs"
|
||
|
||
state GuardianActivityLog {
|
||
[*] --> ViewingUserLogs
|
||
ViewingUserLogs : GET /guardian/activity-logs (paginated)\nGET /guardian/obstacle-logs\nFilter by type, date
|
||
}
|
||
|
||
GuardianActivityLog --> GuardianIdle : Kembali
|
||
|
||
' --- SEND NOTIFICATION ---
|
||
GuardianIdle --> GuardianSendNotif : Tap "Send Notification"
|
||
|
||
state GuardianSendNotif {
|
||
[*] --> ComposingNotif
|
||
ComposingNotif : Toggle: TEXT / VOICE NOTE
|
||
ComposingNotif --> SendingTextNotif : Ketik pesan → Send
|
||
ComposingNotif --> RecordingVoiceNote : Tahan tombol Record
|
||
SendingTextNotif : POST /guardian/notifications/send\n{type: TEXT, content}\nBackend: FCM ke User + WebSocket\nUser TTS: "New message from Guardian"
|
||
RecordingVoiceNote : record package → audio file\nPreview → Upload → POST send\n{type: VOICE_NOTE, voiceNoteUrl}
|
||
SendingTextNotif --> ComposingNotif : Sent
|
||
RecordingVoiceNote --> ComposingNotif : Sent
|
||
}
|
||
|
||
GuardianSendNotif --> GuardianIdle : Kembali
|
||
|
||
' --- AI CONFIG ---
|
||
GuardianIdle --> GuardianAiConfig : Tap "Configure AI Settings"
|
||
|
||
state GuardianAiConfig {
|
||
[*] --> EditingAiConfig
|
||
EditingAiConfig : GET /guardian/ai-config\nSlider confidenceThreshold (0.3-0.9)\nSlider alertDistanceClose (0.5-3m)\nSlider alertDistanceMedium (1-6m)\nDropdown maxInferenceFps (1,2,3,5,10)\nMulti-select enabled labels
|
||
EditingAiConfig --> SavingAiConfig : Tap Save
|
||
SavingAiConfig : PUT /guardian/ai-config\nBackend: update + FCM ke User\n"Guardian updated AI settings"
|
||
SavingAiConfig --> EditingAiConfig : Saved
|
||
}
|
||
|
||
GuardianAiConfig --> GuardianIdle : Kembali
|
||
|
||
' --- VOICE COMMANDS CONFIG ---
|
||
GuardianIdle --> GuardianVoiceCmdConfig : Tap "Voice Commands"
|
||
|
||
state GuardianVoiceCmdConfig {
|
||
[*] --> EditingVoiceCommands
|
||
EditingVoiceCommands : GET /guardian/voice-commands\nList 14 commands\nEdit trigger phrase\nToggle enabled/disabled
|
||
EditingVoiceCommands --> SavingVoiceCmd : Save
|
||
SavingVoiceCmd : PUT /guardian/voice-commands\nFCM ke User: "Voice commands updated"
|
||
SavingVoiceCmd --> EditingVoiceCommands : Saved
|
||
}
|
||
|
||
GuardianVoiceCmdConfig --> GuardianIdle : Kembali
|
||
|
||
' --- HARDWARE SHORTCUTS ---
|
||
GuardianIdle --> GuardianShortcutConfig : Tap "Shortcuts"
|
||
|
||
state GuardianShortcutConfig {
|
||
[*] --> EditingShortcuts
|
||
EditingShortcuts : GET /guardian/shortcuts\nList 5 shortcuts (Vol Up, Vol Down, dll)\n"Unassigned" jika belum di-set
|
||
EditingShortcuts --> CapturingButton : Tap "Configure" per shortcut
|
||
CapturingButton : FCM ke User's phone "enter capture mode"\nUser tekan tombol fisik → keyCode\nPUT /user/shortcuts {shortcutKey, buttonCode}
|
||
CapturingButton --> EditingShortcuts : Button captured & saved
|
||
}
|
||
|
||
GuardianShortcutConfig --> GuardianIdle : Kembali
|
||
|
||
' --- GEOFENCE ---
|
||
GuardianIdle --> GuardianGeofenceConfig : Tap "Geofence"
|
||
|
||
state GuardianGeofenceConfig {
|
||
[*] --> EditingGeofence
|
||
EditingGeofence : FlutterMap: tap untuk set center\nSlider radius (50m - 5000m)\nToggle enable/disable geofence
|
||
EditingGeofence --> SavingGeofence : Tap Save
|
||
SavingGeofence : PUT /guardian/geofence\nBackend: simpan GeofenceConfig
|
||
SavingGeofence --> EditingGeofence : Saved
|
||
}
|
||
|
||
GuardianGeofenceConfig --> GuardianIdle : Kembali
|
||
|
||
' --- SOS HANDLING ---
|
||
GuardianIdle --> SosAlertReceived : FCM HIGH PRIORITY / WebSocket SOS alert\n/queue/sos/{guardianId}
|
||
|
||
state SosAlertReceived {
|
||
[*] --> DisplayingSosAlert
|
||
DisplayingSosAlert : GuardianDashboard: highlight merah\nMap otomatis buka ke lokasi User\nTampil: "🚨 {User} needs help! Location: lat,lng"\nGET /guardian/sos-events
|
||
DisplayingSosAlert --> AcknowledgingSos : Tap "Acknowledge"
|
||
AcknowledgingSos : PUT /guardian/sos/{id}/acknowledge\nBackend: status → ACKNOWLEDGED\nFCM ke User: "Guardian is on the way"\nUser TTS: "Help is coming."
|
||
AcknowledgingSos --> [*]
|
||
}
|
||
|
||
SosAlertReceived --> GuardianIdle : Selesai
|
||
|
||
' --- CALL (Guardian inisiasi) ---
|
||
GuardianIdle --> GuardianOutgoingCall : Tap "Call User"
|
||
|
||
state GuardianOutgoingCall {
|
||
[*] --> GuardianRequestToken
|
||
GuardianRequestToken : POST /shared/call/token\nPOST /shared/call/notify → FCM ke User "Incoming Call"
|
||
GuardianRequestToken --> GuardianCalling : Join Agora channel
|
||
GuardianCalling : "Calling User..." + ring animasi
|
||
GuardianCalling --> GuardianInCall : User accept
|
||
GuardianCalling --> GuardianIdle : User decline / timeout
|
||
GuardianInCall : Audio call aktif\nMute + Speaker + End
|
||
GuardianInCall --> GuardianIdle : End call
|
||
}
|
||
|
||
GuardianOutgoingCall --> GuardianIdle : Selesai
|
||
|
||
' --- INCOMING CALL (Guardian terima) ---
|
||
GuardianIdle --> GuardianIncomingCall : FCM INCOMING_CALL dari User
|
||
|
||
state GuardianIncomingCall {
|
||
[*] --> GuardianShowIncomingCall
|
||
GuardianShowIncomingCall : IncomingCallScreen\nTTS: "Incoming call from User"\nAccept (hijau) / Decline (merah)
|
||
GuardianShowIncomingCall --> GuardianInCallSession : Accept
|
||
GuardianShowIncomingCall --> GuardianIdle : Decline
|
||
GuardianInCallSession : Agora RTC connected
|
||
GuardianInCallSession --> GuardianIdle : End call
|
||
}
|
||
|
||
GuardianIncomingCall --> GuardianIdle : Selesai
|
||
|
||
' --- LOGOUT ---
|
||
GuardianIdle --> GuardianLoggingOut : Tap Logout
|
||
|
||
GuardianLoggingOut : POST /auth/logout {refreshToken}\nSecureStorage.clearAll()\nWebSocket.disconnect()
|
||
GuardianLoggingOut --> [*]
|
||
}
|
||
|
||
' ============================================================
|
||
' GLOBAL LOGOUT / TOKEN REFRESH
|
||
' ============================================================
|
||
UserShell --> Unauthenticated : Logout / Token Expired (401 → refresh gagal)
|
||
GuardianShell --> Unauthenticated : Logout / Token Expired (401 → refresh gagal)
|
||
|
||
note as TokenRefreshNote
|
||
**Token Refresh (AuthInterceptor):**
|
||
Setiap request: inject "Authorization: Bearer {accessToken}"
|
||
Jika 401 → POST /auth/refresh {refreshToken}
|
||
Jika refresh OK → retry request dengan token baru
|
||
Jika refresh 401 → navigate ke /login
|
||
Access token: 1 jam | Refresh token: 30 hari
|
||
end note
|
||
|
||
' ============================================================
|
||
' GEOFENCE EXIT (Parallel event)
|
||
' ============================================================
|
||
note as GeofenceNote
|
||
**Geofence Alert (Backend event):**
|
||
Setiap POST /user/location (saat WalkGuide aktif, 5 detik):
|
||
Backend: GeofenceService.checkGeofence()
|
||
→ Haversine distance dari center ke current position
|
||
→ Jika keluar radius: FCM ke Guardian "User has left geofence!"
|
||
→ ActivityLog: GEOFENCE_EXIT
|
||
→ Guardian: notifikasi di GuardianDashboard
|
||
end note
|
||
|
||
@enduml
|