You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1764 lines
54 KiB
Markdown
1764 lines
54 KiB
Markdown
# 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
|
|
|
|
1. [Project Overview](#project-overview)
|
|
2. [System Architecture](#system-architecture)
|
|
3. [Technology Stack](#technology-stack)
|
|
4. [Directory Structure](#directory-structure)
|
|
5. [Core Features](#core-features)
|
|
6. [Data Flow](#data-flow)
|
|
7. [Key Components](#key-components)
|
|
8. [Android Native Layer](#android-native-layer)
|
|
9. [API Integration](#api-integration)
|
|
10. [Logging System](#logging-system)
|
|
11. [Deployment](#deployment)
|
|
12. [Recent Critical Fixes](#recent-critical-fixes)
|
|
13. [Configuration System](#configuration-system)
|
|
14. [Known Issues & Solutions](#known-issues--solutions)
|
|
15. [Testing & Monitoring](#testing--monitoring)
|
|
16. [Development Guidelines](#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:
|
|
```dart
|
|
// 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.7`
|
|
- `androidx.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)
|
|
```dart
|
|
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
|
|
```dart
|
|
enum QTypeEnum {
|
|
appointment, // 1 - Scheduled appointments
|
|
lab, // 2 - Laboratory services
|
|
rad, // 3 - Radiology services
|
|
general, // 4 - General queue
|
|
}
|
|
```
|
|
|
|
#### Screen Types
|
|
```dart
|
|
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
|
|
```dart
|
|
// 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
|
|
```dart
|
|
enum KioskScreenStateEnums {
|
|
languageState, // Select language
|
|
queueSelectionState, // Select service queue
|
|
askPrescriptionState,// Ask for prescription (if applicable)
|
|
ticketNumState, // Display generated ticket
|
|
busyState, // Loading
|
|
}
|
|
```
|
|
|
|
#### Kiosk Input Methods
|
|
1. **QR Code Scanner**: Scan patient ID barcode
|
|
2. **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)
|
|
```dart
|
|
_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
|
|
```mermaid
|
|
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
|
|
```mermaid
|
|
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
|
|
```mermaid
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
List<TicketDetailsModel> currentTickets // Currently displayed
|
|
List<TicketDetailsModel> queueTickets // Waiting queue
|
|
bool isCallingInProgress // Calling state lock
|
|
```
|
|
|
|
**Key Methods:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```kotlin
|
|
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:**
|
|
```kotlin
|
|
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:**
|
|
```kotlin
|
|
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:**
|
|
```kotlin
|
|
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:**
|
|
```xml
|
|
<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:**
|
|
```xml
|
|
<activity
|
|
android:name=".MainActivity"
|
|
android:screenOrientation="portrait"
|
|
android:keepScreenOn="true"
|
|
android:launchMode="singleTop"
|
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|...">
|
|
</activity>
|
|
```
|
|
|
|
---
|
|
|
|
## 🌐 API Integration
|
|
|
|
### Base URLs
|
|
|
|
```dart
|
|
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
|
|
```dart
|
|
// 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
|
|
```dart
|
|
// 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
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
class ApiConstants {
|
|
static String apiKey = 'EE17D21C7943485D9780223CCE55DCE5';
|
|
|
|
// Hub events
|
|
static String sendQLinePatientCall = "SendQLinePatientCall";
|
|
static String sendQLineConfig = "SendQLineConfig";
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📝 Logging System
|
|
|
|
### Log Types
|
|
|
|
```dart
|
|
enum LogTypeEnum {
|
|
data, // Health checks, events, lifecycle
|
|
error, // Errors and crashes
|
|
connectivity, // Network status, SignalR events
|
|
}
|
|
```
|
|
|
|
### Log File Locations
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. **Time-based**: Clear logs after 48 hours
|
|
2. **Size-based**: Clear when file exceeds 2 MB
|
|
3. **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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```bash
|
|
# 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
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
# 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
|
|
```bash
|
|
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:**
|
|
1. Android OS killed app due to "idle" detection (even on AC power)
|
|
2. Incorrect uptime tracking (calculated time since midnight, not app start)
|
|
3. No foreground service protection
|
|
4. No automatic failure recovery
|
|
5. Missing kiosk display mode flags
|
|
|
|
### Solutions Implemented
|
|
|
|
#### 1. ✅ Fixed Uptime Tracking Bug
|
|
|
|
**Before (WRONG):**
|
|
```dart
|
|
// 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):**
|
|
```dart
|
|
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:**
|
|
```kotlin
|
|
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:**
|
|
```xml
|
|
<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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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:**
|
|
```dart
|
|
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
|
|
|
|
1. `lib/view_models/screen_config_view_model.dart` - Uptime, health checks, lifecycle
|
|
2. `lib/models/global_config_model.dart` - Clinic prefix feature
|
|
3. `lib/repositories/signalR_repo.dart` - Connection improvements
|
|
4. `android/app/src/main/kotlin/.../MainActivity.kt` - Kiosk display mode
|
|
5. `android/app/src/main/AndroidManifest.xml` - Activity flags
|
|
|
|
---
|
|
|
|
## ⚙️ Configuration System
|
|
|
|
### IP-Based Configuration
|
|
|
|
Each device is identified by its IP address and receives specific configuration:
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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:
|
|
|
|
```dart
|
|
// 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:**
|
|
1. Settings > Apps > QLine
|
|
2. Enable "Auto-start"
|
|
3. Set Battery to "No restrictions"
|
|
4. Disable "Battery optimization"
|
|
|
|
**ADB Command:**
|
|
```bash
|
|
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:**
|
|
```kotlin
|
|
// 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
|
|
```bash
|
|
adb shell ps | grep hmg_qline
|
|
```
|
|
|
|
#### View Logcat (MainActivity)
|
|
```bash
|
|
adb logcat | grep MainActivity
|
|
```
|
|
|
|
#### View Logcat (SignalR)
|
|
```bash
|
|
adb logcat | grep SignalR
|
|
```
|
|
|
|
#### Check Device Idle State
|
|
```bash
|
|
adb shell dumpsys deviceidle
|
|
```
|
|
|
|
#### Check Battery Optimization Status
|
|
```bash
|
|
adb shell dumpsys deviceidle whitelist | grep hmg_qline
|
|
```
|
|
|
|
#### View Last 100 App Logs
|
|
```bash
|
|
adb logcat -t 100 | grep hmg_qline
|
|
```
|
|
|
|
#### Pull All Logs from Device
|
|
```bash
|
|
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
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```dart
|
|
// 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:
|
|
|
|
1. **Add health check logs** for monitoring
|
|
2. **Add error handling** with proper logging
|
|
3. **Add null checks** for all external data
|
|
4. **Test with network disconnection** scenarios
|
|
5. **Test with long uptime** (24+ hours)
|
|
6. **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:**
|
|
1. Last 200 lines of logs before app stopped
|
|
2. Android version and device model
|
|
3. Screenshot of Apps > QLine > Battery settings
|
|
4. Output of: `adb shell dumpsys deviceidle whitelist`
|
|
|
|
**Immediate Actions:**
|
|
1. Check device battery optimization settings
|
|
2. Verify app is whitelisted from doze mode
|
|
3. Check if exact alarm permission granted (Android 12+)
|
|
4. Review logs for `[onAppPaused]` or `[onAppDetached]`
|
|
5. Check for lifecycle events before silence
|
|
|
|
**Advanced Troubleshooting:**
|
|
```bash
|
|
# 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
|
|
1. **Remote Monitoring**: Send health status to backend server
|
|
2. **Watchdog Service**: External process that restarts app if killed
|
|
3. **Scheduled Restart**: Auto-restart at 3 AM daily (already implemented at 00:15)
|
|
4. **Memory Profiling**: Track and log memory usage trends
|
|
5. **Firebase Crashlytics**: Cloud-based crash analytics
|
|
6. **Network Quality Metrics**: Log ping times and bandwidth
|
|
|
|
### Technical Debt
|
|
1. Clean up commented code in MainActivity.kt
|
|
2. Add unit tests for ViewModels
|
|
3. Add integration tests for SignalR connection
|
|
4. Implement proper i18n instead of hardcoded strings
|
|
5. 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
|
|
|