54 KiB
HMG_QLine - Project Context Documentation
Last Updated: July 30, 2026
Project Type: Flutter Healthcare Queue Management System
Target Platform: Android AC-Powered Kiosk Displays
Status: Production (Running 24/7 in HMG Hospitals, Saudi Arabia)
📋 Table of Contents
- Project Overview
- System Architecture
- Technology Stack
- Directory Structure
- Core Features
- Data Flow
- Key Components
- Android Native Layer
- API Integration
- Logging System
- Deployment
- Recent Critical Fixes
- Configuration System
- Known Issues & Solutions
- Testing & Monitoring
- Development Guidelines
📖 Project Overview
Purpose
HMG_QLine is an enterprise-grade hospital queue management system designed for continuous 24/7 operation on AC-powered Android LED kiosk displays across multiple HMG (Habib Medical Group) hospital branches in Saudi Arabia.
Key Objectives
- Real-time Patient Queue Management: Display and manage patient queues across waiting areas
- Multi-modal Patient Calling: Audio tones + Text-to-Speech voice announcements
- Self-Service Kiosks: Allow patients to generate tickets via QR scan or ID entry
- 24/7 Reliability: Run continuously without manual intervention
- Multi-Branch Support: Centralized configuration for multiple hospital locations
- Multi-Language: Full English/Arabic support with RTL
Supported Branches
| Branch | Project ID | Location |
|---|---|---|
| Takhasusi Main | 16 | Riyadh (uses legacy orientation detection) |
| Suwaidi | 17 | Riyadh |
| Khobar | 60 | Eastern Province |
| Takhasusi Women | 140 | Riyadh |
🏗️ System Architecture
Architecture Pattern
MVVM (Model-View-ViewModel) with Provider for state management
┌─────────────────────────────────────────────────────────────┐
│ Views (UI) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Splash │ │ Main Queue │ │ Kiosk │ │
│ │ Screen │ │ Screen │ │ Screen │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ ViewModels (State) │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ ScreenConfigViewModel │ │ QueuingViewModel │ │
│ │ - Configuration │ │ - Queue Management │ │
│ │ - Lifecycle │ │ - SignalR Hub │ │
│ │ - Health Checks │ │ - Audio/TTS │ │
│ └────────────────────────┘ └────────────────────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Services (Business Logic) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Logger │ │ Audio │ │ TTS │ │ Network │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Repositories (Data Layer) │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ SignalR Repository │ │ Screen Details Repo │ │
│ │ - Real-time Hub │ │ - REST API Calls │ │
│ └────────────────────────┘ └────────────────────────┘ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Data Sources │
│ ┌────────────────────────┐ ┌────────────────────────┐ │
│ │ SignalR Hub (WSS) │ │ REST API (HTTPS) │ │
│ │ - Live updates │ │ - Configuration │ │
│ │ - Patient calls │ │ - Ticket generation │ │
│ └────────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Dependency Injection
GetIt is used for service location and dependency injection:
// All singletons registered in lib/config/dependency_injection.dart
- LoggerService
- CrashHandlerService
- ApiClient
- SignalrRepo
- ScreenDetailsRepo
- NativeMethodChannelService
- ConnectivityService
- CacheService
- AudioService
- TextToSpeechService
- ScreenConfigViewModel
- QueuingViewModel
🛠️ Technology Stack
Flutter/Dart
- Flutter SDK: >=3.5.4
- Dart SDK: ^3.5.4
- State Management: Provider 6.1.2
- DI Container: GetIt 8.0.2
Key Packages
| Package | Version | Purpose |
|---|---|---|
signalr_core |
1.1.1 | Real-time WebSocket communication |
provider |
6.1.2 | State management |
get_it |
8.0.2 | Dependency injection |
connectivity_plus |
6.1.0 | Network status monitoring |
wakelock_plus |
1.2.10 | Keep device awake |
flutter_tts |
4.2.0 | Text-to-speech announcements |
just_audio |
0.9.42 | Audio tone playback |
shared_preferences |
2.3.5 | Local key-value storage |
path_provider |
2.1.5 | Access to file system |
qr_code_scanner_plus |
2.0.10+1 | QR code scanning |
logger |
2.4.0 | Logging framework |
intl |
0.19.0 | Internationalization |
flutter_svg |
2.0.14 | SVG rendering |
marquee |
2.2.3 | Scrolling text animation |
Android Native (Kotlin)
- Kotlin: JVM Target 1.8
- Compile SDK: 35 (Android 15)
- Min SDK: Flutter default (likely 21+)
- Dependencies:
androidx.lifecycle:lifecycle-service:2.8.7androidx.core:core-ktx:1.13.1
📁 Directory Structure
HMG_QLine/
├── lib/
│ ├── main.dart # Entry point with crash handling
│ ├── api/
│ │ └── api_client.dart # HTTP client with logging
│ ├── config/
│ │ ├── dependency_injection.dart # GetIt DI setup
│ │ └── routes.dart # App routes configuration
│ ├── constants/
│ │ └── app_constants.dart # All constants (API, colors, assets)
│ ├── models/
│ │ ├── global_config_model.dart # Main configuration model
│ │ ├── ticket_model.dart # Patient ticket data
│ │ ├── kiosk_*.dart # Kiosk-specific models
│ │ ├── prayers_widget_model.dart # Islamic prayer times
│ │ ├── weathers_widget_model.dart # Weather data
│ │ └── rss_feed_model.dart # News feed
│ ├── repositories/
│ │ ├── signalR_repo.dart # SignalR hub management
│ │ └── screen_details_repo.dart # REST API calls
│ ├── services/
│ │ ├── logger_service.dart # File logging with rotation
│ │ ├── crash_handler_service.dart # Global error handling
│ │ ├── connectivity_service.dart # Network monitoring
│ │ ├── audio_service.dart # Audio tone playback
│ │ ├── text_to_speech_service.dart # TTS announcements
│ │ └── cache_service.dart # SharedPreferences wrapper
│ ├── utilities/
│ │ ├── enums.dart # All enums
│ │ ├── extensions.dart # Dart extensions
│ │ ├── native_method_handler.dart # Platform channel
│ │ ├── foreground_task_handler.dart # Background service
│ │ └── lifecycle_handler.dart # App lifecycle
│ ├── view_models/
│ │ ├── screen_config_view_model.dart # Configuration & lifecycle VM
│ │ └── queuing_view_model.dart # Queue management VM
│ └── views/
│ ├── splash_screen/ # Loading screen
│ ├── main_queue_screen/ # Main display (waiting area)
│ ├── kiosk_screens/ # Self-service kiosk UI
│ ├── common_widgets/ # Reusable components
│ └── view_helpers/ # UI utilities
├── android/
│ ├── app/
│ │ ├── build.gradle # Android build config
│ │ └── src/main/
│ │ ├── AndroidManifest.xml # Permissions & components
│ │ └── kotlin/com/example/hmg_qline/hmg_qline/
│ │ ├── MainActivity.kt # Main activity with kiosk flags
│ │ ├── AlarmScheduler.kt # Daily restart scheduler
│ │ ├── BootReceiver.kt # Auto-start on boot
│ │ ├── RestartAlarmReceiver.kt # Scheduled restart handler
│ │ └── BootForegroundService.kt # Boot service
│ ├── build.gradle # Project-level build
│ └── gradle.properties # Gradle properties
├── assets/
│ ├── fonts/ # Poppins, Cairo, GE_SS_Two
│ ├── images/ # Logos, icons (SVG/PNG)
│ ├── new_design_icons/ # Updated design assets
│ │ └── weather_icons/ # Weather SVG icons
│ └── tones/
│ └── call_tone.mp3 # Patient call tone
├── qline_scripts/ # Deployment automation
│ ├── deploy_qline.sh # Deploy APK to devices
│ ├── restart_qline.sh # Restart app
│ ├── extract_logs.sh # Pull logs
│ ├── clear_logs.sh # Clear device logs
│ ├── connect_devices.sh # Connect via ADB
│ ├── kill_qline.sh # Force stop app
│ ├── device_ips_*.txt # IP address lists per branch
│ └── apks/ # APK storage (gitignored)
├── pubspec.yaml # Flutter dependencies
├── README.md # Basic project info
├── QUICK_SUMMARY.md # Quick reference
├── CHANGES_EXPLANATION.md # Detailed changelog
├── AC_POWERED_KIOSK_SUMMARY.md # Kiosk-specific docs
├── DEPLOYMENT_READY.md # (empty)
└── PROJECT_CONTEXT.md # This file
🎯 Core Features
1. Queue Management System
Patient Call Types (Enums)
enum CallTypeEnum {
vitalSign, // 1 - Vital signs measurement room
doctor, // 2 - Doctor consultation
procedure, // 3 - Medical procedures
vaccination, // 4 - Vaccination room
nebulization, // 5 - Nebulization treatment
none, // 6 - No specific type
}
Queue Types
enum QTypeEnum {
appointment, // 1 - Scheduled appointments
lab, // 2 - Laboratory services
rad, // 3 - Radiology services
general, // 4 - General queue
}
Screen Types
enum ScreenTypeEnum {
waitingAreaScreen, // 1 - Main waiting area display
roomLevelScreen, // 2 - Specific counter/room
receptionScreen, // 3 - Reception desk
dashboardScreen, // 4 - Admin dashboard
kioskScreen, // 5 - Self-service kiosk
}
2. Real-Time Communication (SignalR)
Hub Connection Features
- Base URL:
{baseUrl}/PatientCallingHub?IPAddress={deviceIp} - Auto-reconnect: Custom delays
[0, 2000, 5000, 10000, 30000]ms - Keep-alive: 15-second heartbeat
- Timeout: 2 minutes
- Manual retry: 10 attempts with 5-second delay
Hub Events
// Receive patient call
connection.on("SendQLinePatientCall", (message) => {
// Adds ticket to queue
// Plays tone + TTS announcement
// Updates display
});
// Receive configuration update
connection.on("SendQLineConfig", (message) => {
// Updates global configuration
// Refreshes widgets (weather, prayer, RSS)
});
3. Audio & Voice System
Audio Service (Just Audio)
- Plays call tone:
assets/tones/call_tone.mp3 - Mute support based on configuration
- Completion callback triggers TTS
Text-to-Speech Service
- Multi-language: English + Arabic
- Voice speed configuration
- Dynamic message construction based on call type
- Completion callback triggers next ticket in queue
Call Flow
1. Ticket received via SignalR
2. Add to queue (currentTickets or queueTickets)
3. Play audio tone (if enabled)
4. On tone complete → Speak TTS message (if enabled)
5. On TTS complete → Wait {concurrentCallDelaySec} seconds
6. Process next ticket in queue
4. Kiosk Self-Service
Kiosk States
enum KioskScreenStateEnums {
languageState, // Select language
queueSelectionState, // Select service queue
askPrescriptionState,// Ask for prescription (if applicable)
ticketNumState, // Display generated ticket
busyState, // Loading
}
Kiosk Input Methods
- QR Code Scanner: Scan patient ID barcode
- Manual Entry: Type patient ID on virtual keyboard
Ticket Generation Flow
1. Select language (English/Arabic)
2. Select queue (from kioskQueueList)
3. Scan QR / Enter patient ID
4. API call: /api/Gen/GEN_PatientCallNo_Get
5. Display ticket with queue number
6. Auto-return to language selection after timeout
5. Information Widgets
Weather Widget
- API:
/api/PatientCall/WeatherForecast_GetBy5Days - Data: 5-day forecast with min/max temp
- Icons: SVG weather icons (sunny, rainy, cloudy, etc.)
- Update: Every midnight or manual refresh
Prayer Times Widget
- API:
/api/PatientCall/PrayerTime_Today - Data: Fajr, Dhuhr, Asr, Maghrib, Isha
- Display: Next upcoming prayer with countdown
- Source: Based on project lat/long coordinates
RSS Feed Widget
- API:
/api/PatientCall/RssFeed_Get - Display: Marquee scrolling text
- Update: Every 2 hours
- Fallback: Dummy news text if API fails
6. Health Monitoring System
Health Check Timer (Every 5 Minutes)
_startHealthCheckTimer() {
Timer.periodic(Duration(minutes: 5), (timer) {
// 1. Calculate actual uptime
final uptime = DateTime.now().difference(appStartTime);
// 2. Log health status
loggerService.logToFile(
"Health check - App uptime: ${uptime.inHours}h ${uptime.inMinutes % 60}m"
);
// 3. Check SignalR connection
syncHubConnectionState();
// 4. Track consecutive failures
if (failure) {
healthCheckFailures++;
if (healthCheckFailures >= 3) {
// Auto-restart SignalR connection
}
}
// 5. Ensure wakelock still enabled
WakelockPlus.enable();
// 6. Log connection status
logToFile("Hub connected: $isHubConnected, Internet: $isInternetConnected");
// 7. Warn if running >60 hours
if (uptime.inHours > 60) {
logToFile("WARNING: App running for ${uptime.inHours} hours");
}
});
}
🔄 Data Flow
Ticket Call Flow
graph TD
A[Backend sends ticket via SignalR] --> B[onHubTicketCall in QueuingViewModel]
B --> C{Is calling in progress?}
C -->|No| D[Add to currentTickets, show on screen]
C -->|Yes| E[Add to queueTickets for later]
D --> F[Acknowledge ticket via API]
F --> G[Play audio tone]
G --> H[Speak TTS message]
H --> I{More tickets in queue?}
I -->|Yes| J[Wait concurrentCallDelaySec]
J --> K[Move next ticket from queueTickets to currentTickets]
K --> F
I -->|No| L[Set isCallingInProgress = false]
Configuration Update Flow
graph TD
A[App starts] --> B[Get device IP address]
B --> C[Call /api/PatientCall/Common_Config_GetByIP]
C --> D[Parse GlobalConfigurationsModel]
D --> E[Set screen type, queue type, orientation]
E --> F{Widgets required?}
F -->|Weather| G[Fetch weather data]
F -->|Prayers| H[Fetch prayer times]
F -->|RSS| I[Fetch RSS feed]
G --> J[Start periodic update timer]
H --> J
I --> J
J --> K[Update every 10 minutes / midnight]
Lifecycle & Recovery Flow
graph TD
A[App initialized] --> B[Record appStartTime]
B --> C[Start health check timer: 5 min]
C --> D[Start midnight check timer: 10 min]
D --> E[Listen to network changes]
E --> F{Network disconnected?}
F -->|Yes| G[Set isInternetConnected = false]
G --> H[Set isHubConnected = false]
F -->|No| I{Network reconnected?}
I -->|Yes| J[Set isInternetConnected = true]
J --> K[Restart SignalR connection]
K --> L[Health check validates connection]
L --> M{3 consecutive failures?}
M -->|Yes| N[Force restart SignalR]
M -->|No| O[Continue monitoring]
🔑 Key Components
1. ScreenConfigViewModel
Responsibilities:
- Device IP detection
- Global configuration management
- Lifecycle management (onAppPaused, onAppResumed, onAppDetached)
- Health monitoring (5-minute timer)
- Widget data fetching (weather, prayers, RSS)
- Kiosk queue selection
- QR scanner management
- Ticket generation for kiosk
Key Properties:
String currentScreenIP // Device IP address
GlobalConfigurationsModel globalConfigurationsModel
ScreenTypeEnum currentScreenTypeEnum
QTypeEnum currentQTypeEnum
bool isInternetConnected
bool isHubConnected
DateTime appStartTime // For uptime tracking
int healthCheckFailures // Consecutive failure count
Key Methods:
initializeScreenConfigVM() // Initialize on app start
waitForIPAndInitializeConfigVM() // Wait for IP then init
getGlobalConfigurationsByIP() // Fetch config from API
listenNetworkConnectivity() // Monitor network changes
_startHealthCheckTimer() // Health monitoring every 5 min
syncHubConnectionState() // Check SignalR status
onAppPaused() // Log when app backgrounded
onAppResumed() // Re-enable wakelock
onAppDetached() // Log when app killed
2. QueuingViewModel
Responsibilities:
- SignalR hub connection management
- Patient ticket queue management
- Audio tone playback coordination
- TTS voice announcement coordination
- Ticket acknowledgment to backend
Key Properties:
List<TicketDetailsModel> currentTickets // Currently displayed
List<TicketDetailsModel> queueTickets // Waiting queue
bool isCallingInProgress // Calling state lock
Key Methods:
initializeQueueingVM() // Initialize services
startHubConnection() // Connect to SignalR hub
stopHubConnection() // Disconnect from hub
onHubTicketCall() // Handle incoming ticket
onHubConfigCall() // Handle config update
onHubReconnected() // Handle reconnection
onHubDisconnected() // Handle disconnection
addNewTicket() // Add ticket to queue
callTicketOnScreen() // Display & announce ticket
onToneCompleted() // Trigger TTS after tone
onVoiceCompleted() // Process next ticket
waitAndCallNextTicketIfAvailable() // Queue management
3. SignalrRepo
Implementation:
class SignalrRepoImp implements SignalrRepo {
HubConnection? connection;
// Check if connected
bool get isConnected => connection?.state == HubConnectionState.connected;
// Start connection with auto-reconnect
startHubConnection({
required String deviceIp,
required Function(List<Object?>?) onHubTicketCall,
required Function(dynamic) onHubConfigCall,
required Function(dynamic) onHubReconnected,
required Function(dynamic) onHubDisconnected,
}) {
connection = HubConnectionBuilder()
.withUrl("$baseUrl?IPAddress=$deviceIp")
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
.build();
connection!.serverTimeoutInMilliseconds = 120000;
connection!.keepAliveIntervalInMilliseconds = 15000;
// Register event handlers
connection!.on("SendQLinePatientCall", onHubTicketCall);
connection!.on("SendQLineConfig", onHubConfigCall);
// Handle reconnection
connection!.onreconnected(onHubReconnected);
connection!.onclose((exception) {
// Auto-retry up to 10 times
});
await connection!.start();
}
}
4. LoggerService
Features:
- File-based logging to external storage
- Three log types: Data, Error, Connectivity
- Log rotation based on time (48h) or size (2MB)
- IP-based log file naming
Log File Structure:
/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs/
├── 12.4.5.1_data_logs.txt
├── 12.4.5.1_error_logs.txt
└── 12.4.5.1_connectivity_logs.txt
Log Format:
[2026-07-30 02:30:15 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: Health check performed - App uptime: 14h 23m (started: 2026-07-30 00:07:00)
5. CrashHandlerService
Global Error Handling:
setupGlobalErrorHandlers() {
// Flutter framework errors
FlutterError.onError = (FlutterErrorDetails details) {
handleCrash(error: details.exception, stackTrace: details.stack);
};
// Platform dispatcher errors
PlatformDispatcher.instance.onError = (error, stack) {
handleCrash(error: error, stackTrace: stack);
return true;
};
// Zone errors handled by runZonedGuarded in main.dart
}
6. GlobalConfigurationsModel
Key Configuration Fields:
class GlobalConfigurationsModel {
int? id;
int? projectID; // Branch identifier
ScreenTypeEnum screenTypeEnum; // Screen type
QTypeEnum qTypeEnum; // Queue type
ScreenOrientationEnum orientationTypeEnum;
// Display settings
int screenMaxDisplayPatients = 16;
int concurrentCallDelaySec = 1;
// Audio/Voice
bool isToneReq = false;
bool isVoiceReq = false;
LanguageEnum voiceLanguageEnum;
LanguageEnum screenLanguageEnum;
// Widget visibility
bool isWeatherReq = false;
bool isPrayerTimeReq = false;
bool isRssFeedReq = false;
// Location data
double? projectLatitude;
double? projectLongitude;
int? cityKey;
// Clinic prefix validation
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
// Kiosk configuration
List<KioskQueueModel>? kioskQueueList;
List<KioskLanguageConfigModel>? kioskLanguageConfigList;
// Legacy support flag
bool isFromTakhasusiMain = false; // projectID == 16
// Clinic prefix validation method
bool isClinicPrefixAdded(String ticketNo) {
return RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
// Returns true for: "ABC W-123" format
}
}
🤖 Android Native Layer
MainActivity.kt
Kiosk Display Mode Flags:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times (AC-powered kiosk)
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Initialize daily restart alarm at 00:15
initializeRestartAlarm()
}
Method Channel Support:
configureFlutterEngine(flutterEngine: FlutterEngine) {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"reopenApp" -> bringAppToForeground()
"restartApp" -> restartApplication()
"restartDevice" -> restartDevice() // Requires root
"clearAudioCache" -> clearAudioResources()
"clearAllResources" -> clearAllNativeResources()
"scheduleRestartAlarm" -> scheduleRestartAlarm(hour, minute)
"cancelRestartAlarm" -> cancelRestartAlarm()
"canScheduleExactAlarms" -> checkAlarmPermission()
"requestExactAlarmPermission" -> requestAlarmPermission()
}
}
}
AlarmScheduler.kt
Android Version Compatibility:
fun scheduleRestartAlarm(context: Context, hour: Int = 0, minute: Int = 15) {
when {
// Android 14+ (API 34+)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(...)
} else {
alarmManager.setAndAllowWhileIdle(...) // Fallback
}
}
// Android 12-13 (API 31-33)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(...)
}
}
// Android 6-11 (API 23-30)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
alarmManager.setExactAndAllowWhileIdle(...)
}
// Android < 6
else -> {
alarmManager.setExact(...)
}
}
}
BootBroadcastReceiver.kt
Auto-Start on Device Boot:
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
// 1. Schedule daily restart alarm
AlarmScheduler.scheduleRestartAlarm(context, 0, 15)
// 2. Start foreground service to launch app
val serviceIntent = Intent(context, BootForegroundService::class.java)
context.startForegroundService(serviceIntent)
}
}
AndroidManifest.xml
Critical Permissions:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
Activity Configuration:
<activity
android:name=".MainActivity"
android:screenOrientation="portrait"
android:keepScreenOn="true"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|...">
</activity>
🌐 API Integration
Base URLs
static String baseUrlLive = 'https://qline.hmg.com'; // LIVE
static String baseUrlUat = 'https://ms.hmg.com/nscapi'; // UAT
static String baseUrlDev = 'https://ms.hmg.com/nscapi2'; // DEV
static String baseUrl = baseUrlUat; // Current environment
API Endpoints
Configuration APIs
// Get device configuration by IP
GET /api/PatientCall/Common_Config_GetByIP
Parameters: { ipAddress: string, apiKey: string }
Returns: GlobalConfigurationsModel
// Get waiting area screen config
GET /api/PatientCall/WaitingAreaScreen_Config_Get
Parameters: { ipAddress: string, apiKey: string }
Returns: Screen configuration
// Get RSS feed
GET /api/PatientCall/RssFeed_Get
Parameters: { languageId: int }
Returns: RssFeedModel
// Get weather forecast
GET /api/PatientCall/WeatherForecast_GetBy5Days
Parameters: { cityKey: string }
Returns: WeathersWidgetModel (5-day forecast)
// Get prayer times
GET /api/PatientCall/PrayerTime_Today
Parameters: { latitude: double, longitude: double }
Returns: PrayersWidgetModel
Ticket APIs
// Create ticket for kiosk
POST /api/Gen/GEN_PatientCallNo_Get
Body: {
projectID: int,
queueID: int,
patientID: int,
apiKey: string
}
Returns: KioskPatientTicket
// Acknowledge ticket viewed
POST /api/Common/TicketQueueAck_Insert
Body: {
ticketQueueID: int,
ipAddress: string,
qType: int,
apiKey: string
}
// Update call request (appointment only)
POST /api/PatientCall/CallRequest_QueueUpdate
Body: {
ticketId: int,
ipAddress: string,
callType: int,
apiKey: string
}
SignalR Hub
// Connect to hub
WSS /PatientCallingHub?IPAddress={deviceIp}
// Subscribe to events
Hub.on("SendQLinePatientCall", callback) // Patient call notification
Hub.on("SendQLineConfig", callback) // Configuration update
API Constants
class ApiConstants {
static String apiKey = 'EE17D21C7943485D9780223CCE55DCE5';
// Hub events
static String sendQLinePatientCall = "SendQLinePatientCall";
static String sendQLineConfig = "SendQLineConfig";
}
📝 Logging System
Log Types
enum LogTypeEnum {
data, // Health checks, events, lifecycle
error, // Errors and crashes
connectivity, // Network status, SignalR events
}
Log File Locations
# External storage path
/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs/
# Files (named by device IP)
{IP}_data_logs.txt # General data and health checks
{IP}_error_logs.txt # Errors and crashes
{IP}_connectivity_logs.txt # Network and SignalR events
Log Rotation Rules
- Time-based: Clear logs after 48 hours
- Size-based: Clear when file exceeds 2 MB
- Timestamp: Last cleared time stored in SharedPreferences
Log Format
[YYYY-MM-DD HH:MM:SS AM/PM] [SOURCE: method -> file.dart] TYPE: message
Examples:
[2026-07-30 02:30:15 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] DATA: Health check performed - App uptime: 14h 23m (started: 2026-07-30 00:07:00)
[2026-07-30 02:30:16 PM] [SOURCE: _startHealthCheckTimer -> screen_config_view_model.dart] CONNECTIVITY: Health check - Hub connected: true, Internet: true
[2026-07-30 02:35:20 PM] [SOURCE: onHubDisconnected -> queueing_view_model.dart] CONNECTIVITY: Hub disconnected: Connection lost
==================== CRASH ====================
[2026-07-30 02:40:00 PM]
CONTEXT: QueuingViewModel.triggerAsyncCrash
ERROR: Exception: Test async crash
STACKTRACE:
#0 QueuingViewModel.triggerAsyncCrash
#1 ...
==================================================
Logging Methods
// Data logging
loggerService.logToFile(
message: "Health check performed",
source: "method -> file.dart",
type: LogTypeEnum.data,
);
// Error logging
loggerService.logError("Error message");
// Crash logging
loggerService.logCrash(
error: exception,
stackTrace: stack,
context: "method context",
);
🚀 Deployment
Build Process
# Clean build
cd /Users/faiz-hashmi/StudioProjects/HMG_QLine
flutter clean
flutter pub get
flutter build apk --release
# Output location
build/app/outputs/flutter-apk/app-release.apk
Shell Scripts (qline_scripts/)
1. Connect Devices
sh connect_devices.sh -ipfile device_ips_all.txt
# Connects to all devices via ADB over TCP/IP
# Uses format: adb connect {ip}:5555
2. Deploy APK
sh deploy_qline.sh -ipfile device_ips_all.txt -version 9.4
# Steps:
# 1. Connect to all devices
# 2. Install APK: adb -s {ip}:5555 install -r apks/app-release-9.4.apk
# 3. Launch app: adb -s {ip}:5555 shell am start -n {package}/{activity}
# 4. Log installation in qline_installation_records.txt
3. Restart App
sh restart_qline.sh -ipfile device_ips_all.txt
# Steps:
# 1. Force stop: adb shell am force-stop {package}
# 2. Launch: adb shell am start -n {package}/{activity}
4. Extract Logs
# Single device
sh extract_logs.sh -ip 10.70.6.143
# All devices
sh extract_all_logs.sh -ipfile device_ips_all.txt
# Pulls files from:
# /storage/emulated/0/Android/data/{package}/files/logs/
# To: logs/{ip}_{timestamp}/
5. Clear Logs
# Single device
sh clear_logs.sh -ip 10.70.6.143
# All devices
sh clear_logs_all.sh -ipfile device_ips_all.txt
# Removes all log files from device
6. Kill App
sh kill_qline.sh -ipfile device_ips_all.txt
# Force stop on all devices
# adb shell am force-stop {package}
Device IP Lists
device_ips_all.txt # All hospital devices
device_ips_tak.txt # Takhasusi Main (projectID: 16)
device_ips_tak_women.txt # Takhasusi Women (projectID: 140)
device_ips_swd.txt # Suwaidi (projectID: 17)
device_ips_khobar.txt # Khobar (projectID: 60)
device_ips_cs_test.txt # Cloud Solutions test devices
one_device_ip.txt # Single device for testing
Format (one IP per line):
10.70.6.143
10.70.6.144
10.70.6.145
🔥 Recent Critical Fixes (March 2026)
Problem: App Dying After 14 Hours
Symptoms:
- App ran successfully for ~14 hours
- Complete log silence after 8:36 AM
- No crash logs, no lifecycle events
- Required manual restart by user
Root Causes:
- Android OS killed app due to "idle" detection (even on AC power)
- Incorrect uptime tracking (calculated time since midnight, not app start)
- No foreground service protection
- No automatic failure recovery
- Missing kiosk display mode flags
Solutions Implemented
1. ✅ Fixed Uptime Tracking Bug
Before (WRONG):
// Calculated time since last midnight check
DateTime.now().difference(lastChecked).inHours
// At 6:31 AM, showed "6 hours" but app ran since midnight (6.5 hours)
After (CORRECT):
DateTime appStartTime = DateTime.now(); // Track at initialization
// Calculate actual uptime
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
final uptimeMinutes = DateTime.now().difference(appStartTime).inMinutes % 60;
logToFile("Health check - App uptime: ${uptimeHours}h ${uptimeMinutes}m (started: $appStartTime)");
2. ✅ Android Kiosk Display Mode
File: android/app/src/main/kotlin/.../MainActivity.kt
Added:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Prevent Android from treating app as "idle"
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
Log.d(TAG, "MainActivity created - Kiosk display mode active")
}
File: android/app/src/main/AndroidManifest.xml
Added:
<activity
android:name=".MainActivity"
android:keepScreenOn="true"
android:screenOrientation="portrait">
</activity>
3. ✅ Health Check System
File: lib/view_models/screen_config_view_model.dart
Features:
Timer? _healthCheckTimer;
int healthCheckFailures = 0;
void _startHealthCheckTimer() {
_healthCheckTimer = Timer.periodic(Duration(minutes: 5), (timer) {
try {
// 1. Calculate actual uptime
final uptime = DateTime.now().difference(appStartTime);
// 2. Log health check
logToFile("Health check - App uptime: ${uptime.inHours}h ${uptime.inMinutes % 60}m");
// 3. Check SignalR connection
try {
syncHubConnectionState();
healthCheckFailures = 0; // Reset on success
} catch (e) {
healthCheckFailures++;
// Auto-recover after 3 failures
if (healthCheckFailures >= 3) {
logToFile("CRITICAL: 3 consecutive failures - attempting recovery");
await queuingViewModel.stopHubConnection();
await Future.delayed(Duration(seconds: 2));
await queuingViewModel.startHubConnection();
}
}
// 4. Ensure wakelock enabled
await WakelockPlus.enable();
// 5. Log connection status
logToFile("Hub connected: $isHubConnected, Internet: $isInternetConnected");
// 6. Warn if running >60 hours
if (uptime.inHours > 60) {
logToFile("WARNING: App running for ${uptime.inHours} hours");
}
// 7. Keep UI thread active
notifyListeners();
} catch (e) {
healthCheckFailures++;
logError("Health check failed: $e");
}
});
}
4. ✅ Enhanced Lifecycle Tracking
File: lib/view_models/screen_config_view_model.dart
Added:
Future<void> onAppPaused() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
logToFile("[onAppPaused] - App uptime: ${uptimeHours}h - WARNING: App going to background!");
}
Future<void> onAppResumed() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
logToFile("[onAppResumed] - App uptime: ${uptimeHours}h");
// Re-enable wakelock
await WakelockPlus.enable();
// Verify connections
syncHubConnectionState();
}
Future<void> onAppDetached() async {
final uptimeHours = DateTime.now().difference(appStartTime).inHours;
logToFile("[onAppDetached] - App uptime: ${uptimeHours}h - CRITICAL: App being killed!");
}
Impact:
- Now see EXACTLY when app goes to background or gets killed
- If app stays alive correctly, should NEVER see these logs
5. ✅ SignalR Connection Improvements
File: lib/repositories/signalR_repo.dart
Enhanced:
connection = HubConnectionBuilder()
.withUrl("$hubBaseURL?IPAddress=$deviceIp")
.withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Custom retry delays
.build();
connection!.serverTimeoutInMilliseconds = 120000; // 2 minutes
connection!.keepAliveIntervalInMilliseconds = 15000; // 15 seconds
// Manual reconnection on close
connection!.onclose((exception) async {
reconnectAttempts++;
if (reconnectAttempts < maxReconnectAttempts) {
logToFile("SignalR reconnect attempt #$reconnectAttempts");
await Future.delayed(Duration(seconds: 5));
try {
await connection!.start();
logToFile("SignalR reconnected after disconnect");
reconnectAttempts = 0;
} catch (e) {
logError("Reconnect failed: $e");
}
}
});
6. ✅ Clinic Prefix Feature
File: lib/models/global_config_model.dart
Added:
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
bool isClinicPrefixAdded(String ticketNo) {
// Check for format: "XXX W-XX" (3 letters + space + W-)
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
}
// Returns true for: "ABC W-123", "FMC W-T-4"
// Returns false for: "W-123", "A W-123", "ABCD W-123"
Files Modified
lib/view_models/screen_config_view_model.dart- Uptime, health checks, lifecyclelib/models/global_config_model.dart- Clinic prefix featurelib/repositories/signalR_repo.dart- Connection improvementsandroid/app/src/main/kotlin/.../MainActivity.kt- Kiosk display modeandroid/app/src/main/AndroidManifest.xml- Activity flags
⚙️ Configuration System
IP-Based Configuration
Each device is identified by its IP address and receives specific configuration:
// Device IP detection (automatic or test)
if (useTestIP) {
currentScreenIP = AppConstants.testIP; // "12.4.5.1"
} else {
currentScreenIP = await connectivityService.getCurrentScreenIP();
}
// Fetch configuration
GlobalConfigurationsModel config = await screenDetailsRepo.getGlobalScreenConfigurations(
ipAddress: currentScreenIP
);
Project ID Detection
// Takhasusi Main uses legacy orientation detection
value.isFromTakhasusiMain = value.projectID == AppConstants.projectIdTakhasusiMain; // 16
if (value.isFromTakhasusiMain) {
log("✅ This device will use getTurnsByOrientationForOlderVersions()");
} else {
log("✅ This device will use getTurnsByOrientationForNewVersions()");
}
Configuration Hierarchy
1. Device IP → API call → GlobalConfigurationsModel
2. Screen Type (waiting/room/kiosk/dashboard)
3. Queue Type (appointment/lab/rad/general)
4. Orientation (portrait/landscape)
5. Display Settings (max patients, delay)
6. Audio/Voice Settings (tone, voice, language)
7. Widget Visibility (weather, prayers, RSS)
8. Kiosk Queues & Languages
Dynamic Configuration Updates
Configuration can be updated via SignalR:
// Backend sends new config
Hub → SendQLineConfig → onHubConfigCall()
// Updates applied
globalConfigurationsModel = GlobalConfigurationsModel.fromJson(data);
updateGlobalConfigurationsModel(value: globalConfigurationsModel, needNotify: true);
getInfoWidgetsDetailsFromServer(); // Refresh widgets
⚠️ Known Issues & Solutions
Issue 1: App Dies After 14 Hours
Status: ✅ FIXED (March 2026)
Solution:
- Added kiosk display mode flags
- Implemented health check system
- Fixed uptime tracking
- Added lifecycle logging
Issue 2: Android Doze Mode
Status: ⚠️ REQUIRES DEVICE CONFIGURATION
Solution: Some Android devices (Xiaomi, Oppo, Vivo) have aggressive task killers:
Manual Steps:
- Settings > Apps > QLine
- Enable "Auto-start"
- Set Battery to "No restrictions"
- Disable "Battery optimization"
ADB Command:
adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline
Issue 3: Memory Leaks (48+ Hour Uptime)
Status: ✅ MITIGATED
Solution:
- Daily restart scheduled at 00:15 AM
- Resource cleanup on lifecycle events
- System.gc() calls at strategic points
Issue 4: TTS Stopping on Some Devices
Status: ⚠️ DEVICE-SPECIFIC
Solution:
- Clear audio cache via
clearAudioCache()method channel - Force stop TTS service:
am force-stop com.google.android.tts - Restart app if TTS completely fails
Issue 5: SignalR Connection Drops
Status: ✅ MITIGATED
Solution:
- Auto-reconnect with exponential backoff
- Health check monitors connection every 5 min
- Auto-restart after 3 consecutive failures
Issue 6: Exact Alarm Permission (Android 14+)
Status: ⚠️ REQUIRES USER PERMISSION
Solution:
// Check permission
if (!alarmManager.canScheduleExactAlarms()) {
// Open settings
Intent intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
startActivity(intent)
}
📊 Testing & Monitoring
Testing Checklist
Deployment Testing
- Build APK successfully
- Deploy to test device via ADB
- App starts automatically
- Check log: "MainActivity created - Kiosk display mode active"
- Verify health checks appear every 5 minutes
Short-Term Testing (1 Hour)
- Uptime increments correctly (0h 5m → 0h 10m → 0h 15m → 1h 0m)
- SignalR connection established
- Hub connected: true in logs
- Internet connected: true in logs
- No
[onAppPaused]or[onAppDetached]logs
Medium-Term Testing (24 Hours)
- Health check logs continuous (no gaps)
- App uptime reaches 24h
- Hub connection stable
- No lifecycle events (paused/detached)
- Midnight refresh occurs (if widgets enabled)
Long-Term Testing (48+ Hours)
- App runs past 14-hour checkpoint (critical)
- App reaches 48h uptime
- Check for >60h warning log
- Scheduled restart at 00:15 works correctly
- Auto-start on boot works (if device rebooted)
Network Resilience Testing
- Disconnect WiFi for 10 minutes
- Check log: "Hub disconnected"
- Reconnect WiFi
- Check log: "SignalR reconnect attempt #X"
- Check log: "SignalR reconnected after disconnect"
- Verify auto-recovery after 3 failures
Audio/Voice Testing
- Audio tone plays when ticket called
- TTS voice announcement speaks (if enabled)
- Language switching works (English/Arabic)
- Mute settings respected
- Sequential calls work with delay
Kiosk Testing
- Language selection screen appears
- Queue selection shows all options
- QR scanner works
- Manual ID entry works
- Ticket generation successful
- Thank you screen displays
- Auto-return to language selection after timeout
Monitoring Commands
Check if App Running
adb shell ps | grep hmg_qline
View Logcat (MainActivity)
adb logcat | grep MainActivity
View Logcat (SignalR)
adb logcat | grep SignalR
Check Device Idle State
adb shell dumpsys deviceidle
Check Battery Optimization Status
adb shell dumpsys deviceidle whitelist | grep hmg_qline
View Last 100 App Logs
adb logcat -t 100 | grep hmg_qline
Pull All Logs from Device
cd qline_scripts
sh extract_logs.sh -ip 10.70.6.143
🔧 Development Guidelines
Code Style
Naming Conventions
- Classes: PascalCase (
ScreenConfigViewModel) - Methods: camelCase (
startHubConnection()) - Variables: camelCase (
currentScreenIP) - Constants: camelCase (
static String baseUrl) - Enums: PascalCase (
ScreenTypeEnum) - Files: snake_case (
screen_config_view_model.dart)
File Organization
// 1. Imports
import 'dart:async';
import 'package:flutter/material.dart';
// 2. Class declaration
class MyClass extends ChangeNotifier {
// 3. Private constants
static const String _CONSTANT = "value";
// 4. Dependencies
final MyService myService;
// 5. Constructor
MyClass({required this.myService});
// 6. Public properties
String publicProperty = "";
// 7. Private properties
String _privateProperty = "";
// 8. Public methods
void publicMethod() {}
// 9. Private methods
void _privateMethod() {}
// 10. Overrides
@override
void dispose() {
super.dispose();
}
}
Logging Best Practices
// Always include source file in logs
loggerService.logToFile(
message: "Meaningful message with data: $value",
source: "methodName -> file_name.dart",
type: LogTypeEnum.data,
);
// Use appropriate log types
LogTypeEnum.data // Health checks, state changes, events
LogTypeEnum.error // Errors, exceptions
LogTypeEnum.connectivity // Network, SignalR events
// Include context in error logs
try {
// code
} catch (e, stackTrace) {
loggerService.logCrash(
error: e,
stackTrace: stackTrace,
context: "methodName -> file_name.dart",
);
}
Error Handling
// Always use try-catch for async operations
Future<void> myAsyncMethod() async {
try {
final result = await somethingRisky();
// handle success
} catch (e, stackTrace) {
loggerService.logCrash(
error: e,
stackTrace: stackTrace,
context: "myAsyncMethod -> my_file.dart",
);
// handle error gracefully
}
}
// Provide fallbacks
final value = config.someValue ?? defaultValue;
// Validate before use
if (data != null && data.isNotEmpty) {
processData(data);
}
State Management
// Always notifyListeners after state change
void updateValue(String newValue) {
myValue = newValue;
notifyListeners(); // Trigger UI rebuild
}
// Use getIt for singleton access
final viewModel = getIt.get<ScreenConfigViewModel>();
// Consumer for reactive UI
Consumer<ScreenConfigViewModel>(
builder: (context, viewModel, child) {
return Text(viewModel.currentScreenIP);
},
)
API Calls
// Always check response for null
final response = await screenDetailsRepo.getConfig(ip: currentScreenIP);
if (response == null) {
loggerService.logError("Config API returned null");
updateViewState(ViewState.error);
return;
}
// Use response
updateConfig(response);
Testing Additions
When adding new features:
- Add health check logs for monitoring
- Add error handling with proper logging
- Add null checks for all external data
- Test with network disconnection scenarios
- Test with long uptime (24+ hours)
- Update this documentation with new features
📚 Additional Documentation Files
- README.md - Basic project information
- QUICK_SUMMARY.md - Quick reference for March 2026 changes
- CHANGES_EXPLANATION.md - Detailed explanation of fixes
- AC_POWERED_KIOSK_SUMMARY.md - Kiosk-specific documentation
- PROJECT_CONTEXT.md - This file (comprehensive context)
🔄 Version History
| Version | Date | Changes |
|---|---|---|
| 9.4 | July 2026 | Current production version |
| 9.2 | March 2026 | Critical fixes: uptime tracking, health checks, kiosk flags |
| 9.0 | February 2026 | SignalR improvements, lifecycle tracking |
| 8.x | 2025 | Previous stable versions |
📞 Emergency Contacts & Procedures
If App Dies Again
Collect Information:
- Last 200 lines of logs before app stopped
- Android version and device model
- Screenshot of Apps > QLine > Battery settings
- Output of:
adb shell dumpsys deviceidle whitelist
Immediate Actions:
- Check device battery optimization settings
- Verify app is whitelisted from doze mode
- Check if exact alarm permission granted (Android 12+)
- Review logs for
[onAppPaused]or[onAppDetached] - Check for lifecycle events before silence
Advanced Troubleshooting:
# Force whitelist from doze
adb shell dumpsys deviceidle whitelist +com.example.hmg_qline.hmg_qline
# Check app status
adb shell ps | grep hmg_qline
# Check if app is optimized
adb shell dumpsys battery | grep hmg_qline
# Force prevent background kill
adb shell cmd appops set com.example.hmg_qline.hmg_qline RUN_IN_BACKGROUND allow
🎯 Future Improvements
Planned Enhancements
- Remote Monitoring: Send health status to backend server
- Watchdog Service: External process that restarts app if killed
- Scheduled Restart: Auto-restart at 3 AM daily (already implemented at 00:15)
- Memory Profiling: Track and log memory usage trends
- Firebase Crashlytics: Cloud-based crash analytics
- Network Quality Metrics: Log ping times and bandwidth
Technical Debt
- Clean up commented code in MainActivity.kt
- Add unit tests for ViewModels
- Add integration tests for SignalR connection
- Implement proper i18n instead of hardcoded strings
- Refactor large ViewModels into smaller components
📖 Glossary
- AC-Powered Kiosk: Android device permanently plugged into power, not battery-powered
- App Standby Buckets: Android 9+ feature that restricts background apps
- Doze Mode: Android 6+ battery optimization that restricts background processing
- Health Check: Periodic verification that app is functioning correctly
- Hub: SignalR WebSocket server for real-time communication
- Kiosk Mode: Display mode where app runs continuously without user intervention
- QType: Queue type (appointment, lab, rad, general)
- SignalR: Real-time communication library using WebSockets
- TTS: Text-to-Speech for voice announcements
- Uptime: Duration the app has been running since last start
- Wakelock: Android API to prevent device from sleeping
END OF CONTEXT DOCUMENTATION
This document should be updated whenever significant changes are made to the project. Last updated by: AI Assistant Last update date: July 30, 2026