Compare commits

..

12 Commits

Author SHA1 Message Date
FaizHashmi 12598b97d3 few changes of UI 3 days ago
FaizHashmi c0cd969558 added context file 2 months ago
Faiz Hashmi 7a6de329b5 latest version v9.4 3 months ago
Faiz Hashmi dd01a64e8a Convert qline_scripts from submodule to regular directory in root repo 4 months ago
Faiz Hashmi b3ded8e675 scripts 4 months ago
Faiz Hashmi c8709bca85 added scripts 4 months ago
Faiz Hashmi 2a99222b57 added projectID and sent version 9.4 to Tak main branch 6 months ago
Faiz Hashmi 726bef8562 Merge branch 'qline_android14_fixes' into master_clone
# Conflicts:
#	android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/MainActivity.kt
#	lib/constants/app_constants.dart
#	lib/main.dart
#	lib/models/global_config_model.dart
#	lib/view_models/screen_config_view_model.dart
#	lib/views/main_queue_screen/components/priority_tickets.dart
#	lib/views/main_queue_screen/components/priority_tickets_sidelist.dart
#	lib/views/main_queue_screen/main_queue_screen.dart
6 months ago
Faiz Hashmi 5264013397 tak main design fixes. 6 months ago
Faiz Hashmi aa81f40118 fixed size issue on calling cards 7 months ago
faizatflutter 856395f273 android 14 stable version 8 months ago
faizatflutter d9c0ce5180 android 14 testing started. 8 months ago

7
.gitignore vendored

@ -10,8 +10,11 @@
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
@ -41,3 +44,7 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/qline_scripts/apks
/qline_scripts/logs

File diff suppressed because it is too large Load Diff

@ -3,6 +3,14 @@
HMG_QLine is a Flutter-based application designed for efficient patient management and queue handling in healthcare environments. It features multiple screens for patient display, robust logging, and
background service integration to ensure reliability and uptime.
## 📚 Documentation
**For complete project context, architecture, and implementation details, see:**
- **[PROJECT_CONTEXT.md](PROJECT_CONTEXT.md)** - Comprehensive documentation for developers and AI models
- **[QUICK_SUMMARY.md](QUICK_SUMMARY.md)** - Quick reference for recent changes
- **[CHANGES_EXPLANATION.md](CHANGES_EXPLANATION.md)** - Detailed changelog
- **[AC_POWERED_KIOSK_SUMMARY.md](AC_POWERED_KIOSK_SUMMARY.md)** - Kiosk-specific documentation
## Features
- Patient queue management across all screens
@ -78,4 +86,124 @@ Pull requests are welcome. For major changes, please open an issue first to disc
## License
This project is licensed under the MIT License.
This project is licensed under the MIT License.
# Queuing System Shell Scripts
This repository contains a set of shell scripts for managing Android devices running the HMG QLine application in a multi-device hospital environment. The scripts automate deployment, logging, restarting, and uninstallation tasks for efficient device management.
## Features
- **Deploy APKs:** Install and launch specific versions of the QLine app on multiple devices.
- **Restart App:** Force-stop and relaunch the QLine app on all connected devices.
- **Uninstall App:** Remove the QLine app from all connected devices.
- **Extract Logs:** Pull log files from devices for troubleshooting and analysis.
- **Device Connection:** Automatically connect to devices over Wi-Fi using IP addresses.
## Folder Structure
```
queuing_system/
├── apks/ # APK files for different QLine app versions
├── logs/ # Data logs from devices
├── connect_devices.sh # Script to connect to devices
├── deploy_qline.sh # Script to deploy APKs
├── device_ips.txt # List of device IP addresses
├── extract_logs.sh # Script to extract logs from devices
├── restart_qline.sh # Script to restart the app on devices
├── uninstall_qline.sh # Script to uninstall the app
```
## Getting Started
1. **Clone the repository:**
```bash
git clone <repo-url>
cd queuing_system
```
2. **Configure device IPs:**
- Add device IPs to a file (e.g., `device_ips_all.txt`), one per line.
3. **Prepare APKs:**
- Place APK files in the `apks/` folder, named as per the versioning scheme.
4. **Run scripts:**
- Make scripts executable: `chmod +x *.sh`
- Example: Deploy APKs
```bash
./deploy_qline.sh
```
- Example: Extract logs from a device
```bash
sh extract_logs.sh -ip <device_ip:port>
```
## Script Commands (One by One)
Use the commands below with the appropriate IP list file.
### Connect Devices
```bash
sh connect_devices.sh -ipfile device_ips_all.txt
```
### Deploy QLine APK
```bash
sh deploy_qline.sh -ipfile device_ips_all.txt -version 9.2
```
**Deploy to Single IP:**
```bash
echo "10.70.6.143" > temp_ip.txt
sh deploy_qline.sh -ipfile temp_ip.txt -version 9.2
rm temp_ip.txt
```
### Restart QLine App
```bash
sh restart_qline.sh -ipfile device_ips_all.txt
```
### Uninstall QLine App
```bash
sh uninstall_qline.sh -ipfile device_ips_all.txt
```
### Extract Logs (Single Device)
```bash
sh extract_logs.sh -ip <device_ip:port>
```
### Extract Logs (All Devices)
```bash
sh extract_all_logs.sh -ipfile device_ips_all.txt
```
### Clear Logs (Single Device)
```bash
sh clear_logs.sh -ip <device_ip:port>
```
### Clear Logs (All Devices)
```bash
sh clear_logs_all.sh -ipfile device_ips_all.txt
```
### Kill QLine App (All Devices)
```bash
sh kill_qline.sh -ipfile device_ips_all.txt
```
## Requirements
- macOS (tested)
- Android SDK Platform Tools (adb)
- Wi-Fi connection to devices
## Notes
- Ensure all devices are on the same Wi-Fi network (`Cloud Solutions`).
- APKs and logs are ignored from version control via `.gitignore`.
- Scripts are designed for hospital multi-floor deployments but can be adapted for other environments.
## License
MIT License

@ -45,4 +45,5 @@ flutter {
dependencies {
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
implementation 'androidx.core:core-ktx:1.13.1'
}

@ -5,6 +5,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 12+ (API 31+) exact alarm permissions -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- Android 14+ (API 34+) - USE_EXACT_ALARM is for apps that need exact alarms as core functionality -->
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- Foreground service types for Android 14+ -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<queries>
<intent>
@ -24,14 +30,30 @@
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</receiver>
<!-- Receiver for scheduled restart alarm -->
<receiver
android:name=".RestartAlarmReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="com.example.hmg_qline.SCHEDULED_RESTART" />
</intent-filter>
</receiver>
<service
android:name=".BootForegroundService"
android:exported="true" />
android:exported="true"
android:foregroundServiceType="specialUse">
<!-- Android 14+ requires property declaration for FOREGROUND_SERVICE_TYPE_SPECIAL_USE -->
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="App auto-start and scheduled restart" />
</service>
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"

@ -0,0 +1,225 @@
package com.example.hmg_qline.hmg_qline
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.util.Log
import java.util.Calendar
/**
* Utility class for scheduling app restart alarms.
* Handles Android version-specific alarm scheduling with proper backward compatibility.
*
* Android Version Compatibility:
* - Android 14+ (API 34): Uses USE_EXACT_ALARM or SCHEDULE_EXACT_ALARM with permission check
* - Android 12-13 (API 31-33): Uses SCHEDULE_EXACT_ALARM with permission check
* - Android 6-11 (API 23-30): Uses setExactAndAllowWhileIdle
* - Android < 6 (API < 23): Uses setExact
*/
object AlarmScheduler {
private const val TAG = "AlarmScheduler"
private const val RESTART_ALARM_REQUEST_CODE = 1001
/**
* Schedule a daily restart alarm at the specified time.
*
* @param context Application context
* @param hour Hour of day (0-23), default is 0 (midnight)
* @param minute Minute (0-59), default is 15
*/
fun scheduleRestartAlarm(context: Context, hour: Int = 0, minute: Int = 15) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, RestartAlarmReceiver::class.java).apply {
action = RestartAlarmReceiver.ACTION_SCHEDULED_RESTART
}
val pendingIntent = PendingIntent.getBroadcast(
context,
RESTART_ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Calculate next alarm time
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, hour)
set(Calendar.MINUTE, minute)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
// If the time has already passed today, schedule for tomorrow
if (timeInMillis <= System.currentTimeMillis()) {
add(Calendar.DAY_OF_YEAR, 1)
}
}
// Cancel any existing alarm first
alarmManager.cancel(pendingIntent)
Log.d(TAG, "Scheduling restart alarm for: ${calendar.time}")
Log.d(TAG, "Android SDK Version: ${Build.VERSION.SDK_INT}")
// Schedule based on Android version
when {
// Android 14+ (API 34+)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> {
scheduleForAndroid14Plus(context, alarmManager, pendingIntent, calendar.timeInMillis)
}
// Android 12-13 (API 31-33)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
scheduleForAndroid12To13(context, alarmManager, pendingIntent, calendar.timeInMillis)
}
// Android 6-11 (API 23-30)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M -> {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 23-30)")
}
// Android < 6 (API < 23)
else -> {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExact (API < 23)")
}
}
}
/**
* Schedule alarm for Android 14+ (API 34+)
* Android 14 requires special handling for exact alarms.
*/
private fun scheduleForAndroid14Plus(
context: Context,
alarmManager: AlarmManager,
pendingIntent: PendingIntent,
triggerTime: Long
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 34+)")
} else {
// Fallback to inexact alarm if permission not granted
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.w(TAG, "Exact alarm permission not granted, using setAndAllowWhileIdle (API 34+)")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException scheduling alarm: ${e.message}")
// Fallback to inexact alarm
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
}
/**
* Schedule alarm for Android 12-13 (API 31-33)
*/
private fun scheduleForAndroid12To13(
context: Context,
alarmManager: AlarmManager,
pendingIntent: PendingIntent,
triggerTime: Long
) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.d(TAG, "Alarm scheduled using setExactAndAllowWhileIdle (API 31-33)")
} else {
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
Log.w(TAG, "Exact alarm permission not granted, using setAndAllowWhileIdle (API 31-33)")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException scheduling alarm: ${e.message}")
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
}
}
/**
* Cancel the scheduled restart alarm.
*/
fun cancelRestartAlarm(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, RestartAlarmReceiver::class.java).apply {
action = RestartAlarmReceiver.ACTION_SCHEDULED_RESTART
}
val pendingIntent = PendingIntent.getBroadcast(
context,
RESTART_ALARM_REQUEST_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
alarmManager.cancel(pendingIntent)
Log.d(TAG, "Restart alarm cancelled")
}
/**
* Check if the app can schedule exact alarms.
* Returns true for Android < 12 (always allowed) or if permission is granted on Android 12+.
*/
fun canScheduleExactAlarms(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.canScheduleExactAlarms()
} else {
true // Always allowed on older versions
}
}
/**
* Open system settings to request exact alarm permission (Android 12+).
*/
fun requestExactAlarmPermission(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
try {
val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
} catch (e: Exception) {
Log.e(TAG, "Error opening exact alarm settings: ${e.message}")
}
}
}
}

@ -4,46 +4,128 @@ import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.lifecycle.LifecycleService
/**
* Foreground service that launches the app after device boot or scheduled restart.
* Compatible with Android 14+ (API 34+) and older versions.
*
* Android 14+ requires:
* - Explicit foreground service type in manifest and code
* - FOREGROUND_SERVICE_SPECIAL_USE permission
*/
class BootForegroundService : LifecycleService() {
companion object {
private const val TAG = "BootForegroundService"
private const val CHANNEL_ID = "boot_service_channel"
private const val NOTIFICATION_ID = 1
}
override fun onCreate() {
super.onCreate()
startForegroundService()
Log.d(TAG, "Service created - Android SDK: ${Build.VERSION.SDK_INT}")
startForegroundServiceCompat()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val source = intent?.getStringExtra("source") ?: "unknown"
Log.d(TAG, "Service started from source: $source")
// Launch the main activity
launchMainActivity(source)
// Stop the service after launching the app
stopSelf()
return START_NOT_STICKY
}
private fun startForegroundService() {
val channelId = "boot_service_channel"
/**
* Start foreground service with Android version compatibility.
* Android 14+ requires explicit foreground service type.
*/
private fun startForegroundServiceCompat() {
createNotificationChannel()
val notification: Notification = NotificationCompat.Builder(this, channelId)
val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("QLine App")
.setContentText("Monitoring QLine activity...")
.setContentText("Starting QLine...")
.setSmallIcon(R.mipmap.ic_launcher)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setAutoCancel(true)
.build()
startForeground(1, notification)
// Use ServiceCompat for Android 14+ compatibility
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// Android 14+ (API 34+) requires explicit foreground service type
ServiceCompat.startForeground(
this,
NOTIFICATION_ID,
notification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
)
Log.d(TAG, "Foreground service started with SPECIAL_USE type (API 34+)")
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Android 10-13 (API 29-33)
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE)
Log.d(TAG, "Foreground service started with NONE type (API 29-33)")
} else {
// Android 8-9 (API 26-28)
startForeground(NOTIFICATION_ID, notification)
Log.d(TAG, "Foreground service started (API 26-28)")
}
}
/**
* Launch the main activity.
*/
private fun launchMainActivity(source: String) {
try {
Log.d(TAG, "Launching MainActivity from source: $source")
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("launched_from_boot", source == "boot_completed")
putExtra("launched_from_scheduled_restart", source == "scheduled_restart")
}
startActivity(intent)
// Only launch MainActivity if this service is started by the system (e.g. on boot)
val intent = Intent(this, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
Log.d(TAG, "MainActivity launched successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching MainActivity: ${e.message}")
}
startActivity(intent)
stopSelf() // Stop the service after initialization
}
/**
* Create notification channel for Android 8.0+ (API 26+).
*/
private fun createNotificationChannel() {
val channelId = "boot_service_channel"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
CHANNEL_ID,
"Boot Service Channel",
NotificationManager.IMPORTANCE_HIGH
)
NotificationManager.IMPORTANCE_LOW // Use LOW to avoid sound/vibration
).apply {
description = "Used to start QLine app after device boot"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager?.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created")
}
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service destroyed")
}
}

@ -6,17 +6,58 @@ import android.content.Intent
import android.os.Build
import android.util.Log
/**
* BroadcastReceiver that handles device boot events.
* Starts the app automatically after device boot and schedules daily restart alarm.
* Compatible with Android 14+ (API 34+) and older versions.
*/
class BootBroadcastReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "BootBroadcastReceiver"
}
override fun onReceive(context: Context, intent: Intent) {
Log.d("BootReceiver", "Received intent: ${intent.action}")
Log.d(TAG, "Received intent: ${intent.action}")
if (intent.action == Intent.ACTION_BOOT_COMPLETED ||
intent.action == "android.intent.action.QUICKBOOT_POWERON" ||
intent.action == "com.htc.intent.action.QUICKBOOT_POWERON"
) {
Log.d(TAG, "Boot completed detected - Android SDK: ${Build.VERSION.SDK_INT}")
Log.d("BootReceiver", "Starting BootForegroundService.")
val serviceIntent = Intent(context, BootForegroundService::class.java)
// Schedule the daily restart alarm first
scheduleRestartAlarm(context)
// Then start the foreground service to launch the app
startAppViaForegroundService(context)
}
}
/**
* Schedule daily restart alarm at 00:15.
* This ensures the alarm is set even if the app wasn't running before reboot.
*/
private fun scheduleRestartAlarm(context: Context) {
try {
Log.d(TAG, "Scheduling daily restart alarm after boot")
AlarmScheduler.scheduleRestartAlarm(context, 0, 15) // 00:15 (12:15 AM)
Log.d(TAG, "Daily restart alarm scheduled successfully")
} catch (e: Exception) {
Log.e(TAG, "Error scheduling restart alarm after boot: ${e.message}")
}
}
/**
* Start the app via foreground service.
* Uses different approach based on Android version for compatibility.
*/
private fun startAppViaForegroundService(context: Context) {
try {
Log.d(TAG, "Starting BootForegroundService")
val serviceIntent = Intent(context, BootForegroundService::class.java).apply {
putExtra("source", "boot_completed")
}
// Use foreground service for Android 8.0+ (API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -24,6 +65,30 @@ class BootBroadcastReceiver : BroadcastReceiver() {
} else {
context.startService(serviceIntent)
}
Log.d(TAG, "BootForegroundService started successfully")
} catch (e: Exception) {
Log.e(TAG, "Error starting foreground service: ${e.message}")
// Fallback: try direct activity launch
tryDirectActivityLaunch(context)
}
}
/**
* Fallback method to launch activity directly if service fails.
*/
private fun tryDirectActivityLaunch(context: Context) {
try {
Log.d(TAG, "Attempting direct activity launch as fallback")
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("launched_from_boot", true)
}
context.startActivity(launchIntent)
Log.d(TAG, "Direct activity launch successful")
} catch (fallbackError: Exception) {
Log.e(TAG, "Direct activity launch also failed: ${fallbackError.message}")
}
}
}

@ -1,5 +1,8 @@
package com.example.hmg_qline.hmg_qline
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Handler
@ -14,112 +17,169 @@ import java.io.File
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.hmg_qline/foreground"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times (AC-powered kiosk display)
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)
// Disable sleep/screensaver for kiosk mode
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Log if app was restarted
if (intent.getBooleanExtra("restarted", false)) {
Log.d("MainActivity", "App restarted successfully")
}
// Log if launched from boot
if (intent.getBooleanExtra("launched_from_boot", false)) {
Log.d("MainActivity", "App launched from boot")
// Give system time to settle after boot
Thread.sleep(2000)
}
Log.d("MainActivity", "MainActivity created - Kiosk display mode active")
companion object {
private const val TAG = "MainActivity"
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
Log.d("MainActivity", "MethodChannel call received: ${call.method}")
Log.d(TAG, "MethodChannel call received: ${call.method}")
when (call.method) {
"reopenApp" -> {
Log.d("MainActivity", "reopenApp called, bringing app to foreground")
Log.d(TAG, "reopenApp called, bringing app to foreground")
moveTaskToBack(false)
result.success("App brought to foreground")
}
"restartApp" -> {
Log.d("MainActivity", "Restarting application")
Log.d(TAG, "Restarting application")
restartApplication()
result.success("App restart initiated")
}
"restartDevice" -> {
Log.d("MainActivity", "Attempting device restart")
Log.d(TAG, "Attempting device restart")
restartDevice(result)
}
"runShellScript" -> {
Log.d("MainActivity", "Executing shell restart command")
Log.d(TAG, "Executing shell restart command")
executeShellRestart(result)
}
"clearAudioCache" -> {
Log.d("MainActivity", "Clearing audio cache")
Log.d(TAG, "Clearing audio cache")
clearAudioResources()
result.success("Audio cache cleared")
}
"clearAllResources" -> {
Log.d("MainActivity", "Clearing all native resources")
Log.d(TAG, "Clearing all native resources")
clearAllNativeResources()
result.success("All resources cleared")
}
// === NEW: Alarm Scheduling Methods for Android 14+ compatibility ===
"scheduleRestartAlarm" -> {
val hour = call.argument<Int>("hour") ?: 0
val minute = call.argument<Int>("minute") ?: 15
Log.d(TAG, "Scheduling restart alarm for $hour:$minute")
scheduleRestartAlarm(hour, minute)
result.success("Restart alarm scheduled for $hour:$minute")
}
"cancelRestartAlarm" -> {
Log.d(TAG, "Cancelling restart alarm")
AlarmScheduler.cancelRestartAlarm(this)
result.success("Restart alarm cancelled")
}
"canScheduleExactAlarms" -> {
val canSchedule = AlarmScheduler.canScheduleExactAlarms(this)
Log.d(TAG, "Can schedule exact alarms: $canSchedule")
result.success(canSchedule)
}
"requestExactAlarmPermission" -> {
Log.d(TAG, "Requesting exact alarm permission")
AlarmScheduler.requestExactAlarmPermission(this)
result.success("Permission request initiated")
}
else -> {
Log.w("MainActivity", "Method not implemented: ${call.method}")
Log.w(TAG, "Method not implemented: ${call.method}")
result.notImplemented()
}
}
}
}
private fun restartApplication() {
/**
* Schedule daily restart alarm at specified time.
* Compatible with Android 14+ and older versions.
*/
private fun scheduleRestartAlarm(hour: Int, minute: Int) {
try {
Log.d("MainActivity", "Initiating app restart")
AlarmScheduler.scheduleRestartAlarm(this, hour, minute)
Log.d(TAG, "Restart alarm scheduled successfully for $hour:$minute")
} catch (e: Exception) {
Log.e(TAG, "Error scheduling restart alarm: ${e.message}")
}
}
// Clear resources before restart
clearAllNativeResources()
private fun restartApplication() {
try {
Log.d(TAG, "Initiating app restart")
// Create restart intent
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
putExtra("restarted", true)
}
// Get the launch intent
val intent = packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
// Use a shorter delay for faster restart
// Configure intent for clean restart
intent.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra("restarted", true)
}
// Use AlarmManager for reliable restart (works better on Android 14+)
val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager
val pendingIntent = PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
// Schedule restart in 500ms
alarmManager.set(
AlarmManager.RTC,
System.currentTimeMillis() + 500,
pendingIntent
)
Log.d(TAG, "Restart scheduled via AlarmManager")
// Now safely exit the app
Handler(Looper.getMainLooper()).postDelayed({
startActivity(intent)
finishAffinity()
// Remove exitProcess() call if present
// android.os.Process.killProcess(android.os.Process.myPid())
}, 100) // Reduced delay
android.os.Process.killProcess(android.os.Process.myPid())
}, 100)
Log.d("MainActivity", "App restart initiated")
} else {
Log.e("MainActivity", "Could not create restart intent")
Log.e(TAG, "Could not create restart intent")
}
} catch (e: Exception) {
Log.e("MainActivity", "Error during restart: ${e.message}")
// Fallback - don't exit, just log the error
Log.e(TAG, "Error during restart: ${e.message}")
// Fallback: try simple restart
fallbackRestart()
}
}
/**
* Fallback restart method if AlarmManager approach fails
*/
private fun fallbackRestart() {
try {
Log.d(TAG, "Attempting fallback restart")
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
putExtra("restarted", true)
}
if (intent != null) {
startActivity(intent)
finishAffinity()
Runtime.getRuntime().exit(0)
}
} catch (e: Exception) {
Log.e(TAG, "Fallback restart also failed: ${e.message}")
}
}
@ -260,15 +320,59 @@ class MainActivity : FlutterActivity() {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Keep screen on at all times (AC-powered kiosk display)
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)
// Disable sleep/screensaver for kiosk mode
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
// Log if app was restarted
if (intent.getBooleanExtra("restarted", false)) {
Log.d(TAG, "App restarted successfully")
}
// Log if launched from boot
if (intent.getBooleanExtra("launched_from_boot", false)) {
Log.d(TAG, "App launched from boot")
// Give system time to settle after boot
Thread.sleep(2000)
}
Log.d(TAG, "MainActivity created - Kiosk display mode active")
// Schedule daily restart alarm at 00:15 (12:15 AM)
// This ensures the alarm is always set when app starts
initializeRestartAlarm()
}
/**
* Initialize the daily restart alarm.
* Called on app start to ensure alarm is always scheduled.
*/
private fun initializeRestartAlarm() {
try {
Log.d(TAG, "Initializing daily restart alarm")
AlarmScheduler.scheduleRestartAlarm(this, 0, 15) // 00:15 (12:15 AM)
Log.d(TAG, "Daily restart alarm initialized for 00:15")
} catch (e: Exception) {
Log.e(TAG, "Error initializing restart alarm: ${e.message}")
}
}
override fun onResume() {
super.onResume()
Log.d("MainActivity", "Activity resumed")
Log.d(TAG, "Activity resumed")
}
override fun onPause() {
super.onPause()
Log.d("MainActivity", "Activity paused - cleaning up resources")
Log.d(TAG, "Activity paused - cleaning up resources")
// Light cleanup when app goes to background
System.gc()
@ -276,7 +380,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
Log.d("MainActivity", "Activity destroyed")
Log.d(TAG, "Activity destroyed")
// Final cleanup
clearAllNativeResources()

@ -0,0 +1,82 @@
package com.example.hmg_qline.hmg_qline
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
/**
* BroadcastReceiver for handling scheduled app restart alarms.
* This is triggered by AlarmManager at the scheduled time (e.g., 12:15 AM).
* Compatible with Android 14+ (API 34+) and older versions.
*/
class RestartAlarmReceiver : BroadcastReceiver() {
companion object {
private const val TAG = "RestartAlarmReceiver"
const val ACTION_SCHEDULED_RESTART = "com.example.hmg_qline.SCHEDULED_RESTART"
}
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received intent: ${intent.action}")
when (intent.action) {
ACTION_SCHEDULED_RESTART -> {
Log.d(TAG, "Scheduled restart triggered")
launchApp(context)
// Re-schedule the alarm for the next day
rescheduleAlarm(context)
}
Intent.ACTION_BOOT_COMPLETED,
"android.intent.action.QUICKBOOT_POWERON",
"com.htc.intent.action.QUICKBOOT_POWERON" -> {
Log.d(TAG, "Boot completed - re-scheduling daily restart alarm")
rescheduleAlarm(context)
}
}
}
private fun launchApp(context: Context) {
try {
Log.d(TAG, "Launching app via foreground service")
val serviceIntent = Intent(context, BootForegroundService::class.java).apply {
putExtra("source", "scheduled_restart")
}
// Use foreground service for Android 8.0+ (API 26+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent)
} else {
context.startService(serviceIntent)
}
Log.d(TAG, "App launch initiated successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching app: ${e.message}")
// Fallback: try direct activity launch
try {
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
context.startActivity(launchIntent)
} catch (fallbackError: Exception) {
Log.e(TAG, "Fallback launch also failed: ${fallbackError.message}")
}
}
}
private fun rescheduleAlarm(context: Context) {
try {
// Re-schedule for the next day at 00:15
AlarmScheduler.scheduleRestartAlarm(context, 0, 15)
Log.d(TAG, "Alarm rescheduled for next day at 00:15")
} catch (e: Exception) {
Log.e(TAG, "Error rescheduling alarm: ${e.message}")
}
}
}

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.9.10'
ext.kotlin_version = '2.1.0'
repositories {
google()
mavenCentral()

@ -1,3 +1,7 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false

@ -19,7 +19,7 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.7.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}

@ -0,0 +1,44 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

@ -2,11 +2,12 @@ import 'dart:async';
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/api_exception.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:hmg_qline/utilities/api_exception.dart';
typedef FactoryConstructor<U> = U Function(dynamic);
@ -36,6 +37,9 @@ class ApiClientImp implements ApiClient {
var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: headers0, retryTimes: retryTimes);
try {
if (!kReleaseMode) {
log("responseBody:${response.body}");
}
if (!kReleaseMode) {
log("statusCode:${response.statusCode}");
}
@ -98,6 +102,7 @@ class ApiClientImp implements ApiClient {
log(jsonDecode(response.body).toString());
// loggerService.logInfo(jsonDecode(response.body).toString());
}
if (response.statusCode >= 200 && response.statusCode < 500) {
var jsonData = jsonDecode(response.body);
if (jsonData["StatusMessage"] != null && jsonData["StatusMessage"] == "Unauthorized user attempt to access API") {

@ -8,11 +8,11 @@ bool useTestIP = false;
bool isNeedToBreakVoiceForArabic = true;
bool isSpeechCompleted = true;
bool isAndroid14 = false;
class AppStrings {
static String timeRemainingText = "Time Remaining";
static String namazTimeText = "Namaz Time";
static String poweredBy = "Powered By";
static String appName = "QLine";
static String fontNamePoppins = "Poppins";
@ -174,34 +174,11 @@ class AppConstants {
// Maximum log file size in bytes before rotation/clearing. Default 2 MB.
static int maxLogFileSizeBytes = 2 * 1024 * 1024;
// Takhasusi Main Branch IPs - These devices use getTurnsByOrientationForOlderVersions()
static const List<String> takhasusiMainBranchIps = [
'10.70.194.87',
'10.70.6.139',
'10.70.6.141',
'10.70.6.142',
'10.70.6.114',
'10.70.6.143',
'10.70.6.49',
'10.70.6.56',
'10.70.6.144',
'10.70.6.158',
'10.70.6.119',
'10.70.6.156',
'10.70.6.109',
'10.70.6.102',
'10.70.6.107',
'10.70.6.137',
'10.70.6.93',
'10.70.6.147',
'10.70.6.148',
'10.70.6.98',
'10.70.6.112',
'10.70.6.96',
'10.70.6.145',
'10.70.6.91',
'10.70.6.157',
];
// Project IDs for different branches
static const int projectIdTakhasusiMain = 16;
static const int projectIdSuwaidi = 17;
static const int projectIdKhobar = 60;
static const int projectIdTakhasusiWomen = 140;
}
class ApiConstants {
@ -209,7 +186,8 @@ class ApiConstants {
static String baseUrlUat = 'https://ms.hmg.com/nscapi'; // UAT
static String baseUrlDev = 'https://ms.hmg.com/nscapi2'; // DEV
static String baseUrl = baseUrlLive;
static String baseUrl = baseUrlUat;
// static String baseUrl = baseUrlDev;
static String baseUrlHub = '$baseUrl/PatientCallingHub';
static String baseUrlApi = '$baseUrl/api';
@ -268,6 +246,7 @@ class MockJsonRepo {
id: 189805,
patientID: 4292695,
queueNo: 'W-T-4',
laBQGroupID: 0,
mobileNo: '0598544522',
patientEmail: 'munira.ali@hotmail.com',
preferredLang: 2,
@ -279,8 +258,8 @@ class MockJsonRepo {
editedOn: DateTime.parse('2025-08-18 15:09:03.633'),
createdOn: DateTime.parse('2025-08-18 15:06:07.363'),
callTypeEnum: CallTypeEnum.doctor,
queueNoM: 'W-T-4',
callNoStr: 'W_T-4',
queueNoM: 'FMC W-T-4',
callNoStr: 'FMC W-T-4',
isQueue: false,
isToneReq: false,
isVoiceReq: false,

@ -1,17 +1,17 @@
import 'dart:developer';
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:provider/provider.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/config/routes.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/services/crash_handler_service.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:hmg_qline/services/crash_handler_service.dart';
void main() {
runZonedGuarded(() async {
@ -53,11 +53,18 @@ class MyApp extends StatelessWidget {
log("Constraints: maxWidth=${constraints.maxWidth}, maxHeight=${constraints.maxHeight}");
log("Is Portrait (OrientationBuilder): ${orientation == Orientation.portrait}");
log("Is Landscape (OrientationBuilder): ${orientation == Orientation.landscape}");
log("Device Pixel Ratio: ${MediaQuery.of(context).devicePixelRatio}");
log("Physical Size: width=${constraints.maxWidth * MediaQuery.of(context).devicePixelRatio}, height=${constraints.maxHeight * MediaQuery.of(context).devicePixelRatio}");
log("SizeConfig - realWidth: ${SizeConfig.realScreenWidth}, realHeight: ${SizeConfig.realScreenHeight}");
log("SizeConfig - isPortrait: ${SizeConfig.isPortrait}, screenWidth: ${SizeConfig.screenWidth}, screenHeight: ${SizeConfig.screenHeight}");
log("=====================================");
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// if (!isAndroid14) {
// SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// }
return MultiProvider(
providers: [
ChangeNotifierProvider<ScreenConfigViewModel>(

@ -1,3 +1,5 @@
import 'dart:developer';
class GenericRespModel {
GenericRespModel({
this.data,
@ -12,6 +14,7 @@ class GenericRespModel {
String? message;
factory GenericRespModel.fromJson(Map<String, dynamic> json) {
log("jsonjsonjosn: $json");
if (json.containsKey('StatusMessage')) {
if ((json['StatusMessage'] as String).contains('Internal server error')) {
// Utils.showToast("${json['StatusMessage']}");

@ -1,4 +1,5 @@
import 'dart:ui';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/kiosk_language_config_model.dart';
import 'package:hmg_qline/models/kiosk_queue_model.dart';
@ -69,6 +70,8 @@ class GlobalConfigurationsModel {
bool isWeatherReq = false;
bool isPrayerTimeReq = false;
bool isRssFeedReq = false;
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
QTypeEnum qTypeEnum = QTypeEnum.appointment;
ScreenTypeEnum screenTypeEnum = ScreenTypeEnum.waitingAreaScreen;
int? projectID;
@ -79,8 +82,6 @@ class GlobalConfigurationsModel {
List<KioskLanguageConfigModel>? kioskLanguageConfigList;
bool isFromTakhasusiMain = false;
bool globalClinicPrefixReq = false;
bool clinicPrefixReq = true;
String vitalSignTextEng = "Vital Sign";
String doctorTextEng = "Doctor";
@ -172,6 +173,8 @@ class GlobalConfigurationsModel {
this.isWeatherReq = false,
this.isPrayerTimeReq = false,
this.isRssFeedReq = false,
this.globalClinicPrefixReq = false,
this.clinicPrefixReq = true,
this.qTypeEnum = QTypeEnum.appointment,
this.screenTypeEnum = ScreenTypeEnum.waitingAreaScreen,
this.projectID,
@ -181,8 +184,6 @@ class GlobalConfigurationsModel {
this.kioskQueueList,
this.kioskLanguageConfigList,
this.isFromTakhasusiMain = false,
this.globalClinicPrefixReq = false,
this.clinicPrefixReq = true,
this.vitalSignTextEng = "Vital Sign",
this.doctorTextEng = "Doctor",
this.procedureTextEng = "Procedure",
@ -268,6 +269,8 @@ class GlobalConfigurationsModel {
isWeatherReq = json['isWeatherReq'] ?? false;
isPrayerTimeReq = json['isPrayerTimeReq'] ?? false;
isRssFeedReq = json['isRssFeedReq'] ?? false;
globalClinicPrefixReq = json['globalClinicPrefixReq'] ?? false;
clinicPrefixReq = json['clinicPrefixReq'] ?? true;
qTypeEnum = ((json['qType'] ?? qType) as int).toQTypeEnum();
screenTypeEnum = ((json['screenType'] ?? screenType) as int).toScreenTypeEnum();
projectID = json['projectID'] ?? 0;

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:hmg_qline/api/api_client.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/generic_response_model.dart';
@ -37,7 +39,7 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
@override
Future<GlobalConfigurationsModel?> getGlobalScreenConfigurations({required String ipAddress}) async {
try {
// try {
var params = {
"ipAddress": ipAddress.toString(),
"apiKey": AppConstants.apiKey.toString(),
@ -47,18 +49,24 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
ApiConstants.commonConfigGet,
params,
);
List<GlobalConfigurationsModel> globalConfigurationsModel = List.generate(genericModel.data.length, (index) => GlobalConfigurationsModel.fromJson(json: genericModel.data[index]));
List<GlobalConfigurationsModel> globalConfigurationsModel =
List.generate(genericModel.data.length, (index) => GlobalConfigurationsModel.fromJson(json: genericModel.data[index]));
if (globalConfigurationsModel.isNotEmpty) {
loggerService.logToFile(message: globalConfigurationsModel.toString(), type: LogTypeEnum.data, source: "getGlobalScreenConfigurations-> screen_details_repo.dart");
loggerService.logToFile(
message: globalConfigurationsModel.toString(),
type: LogTypeEnum.data,
source: "getGlobalScreenConfigurations-> screen_details_repo.dart");
return globalConfigurationsModel.first;
}
return null;
} catch (e) {
loggerService.logError(e.toString());
loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
InfoComponents.showToast(e.toString());
return null;
}
// } catch (e) {
// log("record:");
// log(e.toString());
// loggerService.logError(e.toString());
// loggerService.logToFile(message: e.toString(), source: "getGlobalScreenConfigurations-> screen_details_repo.dart", type: LogTypeEnum.error);
// InfoComponents.showToast(e.toString());
// return null;
// }
}
@override
@ -105,7 +113,8 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
);
genericRespModel.data = KioskPatientTicket.fromJson(genericRespModel.data);
loggerService.logToFile(message: genericRespModel.toString(), source: "createTicketFromKiosk-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: genericRespModel.toString(), source: "createTicketFromKiosk-> screen_details_repo.dart", type: LogTypeEnum.data);
return genericRespModel;
} catch (e) {
@ -142,9 +151,11 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
body,
);
List<WeathersWidgetModel> weathersWidgetModel = List.generate(genericRespModel.data.length, (index) => WeathersWidgetModel.fromJson(genericRespModel.data[index]));
List<WeathersWidgetModel> weathersWidgetModel =
List.generate(genericRespModel.data.length, (index) => WeathersWidgetModel.fromJson(genericRespModel.data[index]));
if (weathersWidgetModel.isNotEmpty) {
loggerService.logToFile(message: weathersWidgetModel.toString(), source: "getWeatherDetailsByCity-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: weathersWidgetModel.toString(), source: "getWeatherDetailsByCity-> screen_details_repo.dart", type: LogTypeEnum.data);
return weathersWidgetModel.first;
}
return constantWeathersWidgetModel;
@ -166,9 +177,11 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
body,
);
List<PrayersWidgetModel> prayersWidgetModel = List.generate(genericRespModel.data.length, (index) => PrayersWidgetModel.fromJson(genericRespModel.data[index]));
List<PrayersWidgetModel> prayersWidgetModel =
List.generate(genericRespModel.data.length, (index) => PrayersWidgetModel.fromJson(genericRespModel.data[index]));
if (prayersWidgetModel.isNotEmpty) {
loggerService.logToFile(message: prayersWidgetModel.toString(), source: "getPrayerDetailsByLatLong-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: prayersWidgetModel.toString(), source: "getPrayerDetailsByLatLong-> screen_details_repo.dart", type: LogTypeEnum.data);
return prayersWidgetModel.first;
}
@ -193,7 +206,8 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
List<RssFeedModel> rssFeedModel = List.generate(genericRespModel.data.length, (index) => RssFeedModel.fromJson(genericRespModel.data[index]));
if (rssFeedModel.isNotEmpty) {
loggerService.logToFile(message: rssFeedModel.toString(), source: "getRssFeedDetailsByLanguageID-> screen_details_repo.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: rssFeedModel.toString(), source: "getRssFeedDetailsByLanguageID-> screen_details_repo.dart", type: LogTypeEnum.data);
return rssFeedModel.first;
}
@ -229,7 +243,8 @@ class ScreenDetailsRepoImp implements ScreenDetailsRepo {
}
@override
Future<GenericRespModel?> acknowledgeTicketForAppointment({required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
Future<GenericRespModel?> acknowledgeTicketForAppointment(
{required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
try {
var params = {
"id": ticketId.toString(),

@ -1,4 +1,5 @@
import 'dart:developer';
import 'package:flutter_tts/flutter_tts.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/global_config_model.dart';
@ -6,7 +7,6 @@ import 'package:hmg_qline/models/ticket_model.dart';
import 'package:hmg_qline/services/logger_service.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:hmg_qline/utilities/extensions.dart';
import 'package:logger/logger.dart';
abstract class TextToSpeechService {
Future<void> speechText({
@ -15,8 +15,6 @@ abstract class TextToSpeechService {
bool isMute = false,
});
// Future<void> speechTextTest(TicketData ticket);
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
}
@ -30,122 +28,6 @@ class TextToSpeechServiceImp implements TextToSpeechService {
double pitch = 0.6;
Map<String, String> arabicVoice = {"name": "ar-xa-x-ard-local", "locale": "ar"};
@override
// Future<void> speechTextTest(TicketData ticket) async {
// const ttsGoogleEngine = 'com.google.android.tts';
// LanguageEnum langEnum = ticket.voiceLanguageEnum;
// List engines = await textToSpeechInstance.getEngines;
// if (engines.contains(ttsGoogleEngine)) {
// await textToSpeechInstance.setEngine(ttsGoogleEngine);
// }
//
// textToSpeechInstance.setVolume(1.0);
//
// // final voices = await textToSpeechInstance.getVoices;
// // log ("voices:: $voices");
//
// await textToSpeechInstance.setVoice(arabicVoice);
//
// if (langEnum == LanguageEnum.arabic) {
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.arabic.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// } else if (langEnum == LanguageEnum.english) {
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.english.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// }
// String preVoice = ticket.ticketNoText;
// String postVoice = ticket.postVoiceText;
// if (preVoice.isNotEmpty) {
// preVoice = '$preVoice..';
// }
// String ticketNo = ticket.queueNo!.trim().toString();
//
// log("areLanguagesInstalled: ${await textToSpeechInstance.areLanguagesInstalled(["en", "ar"])}");
//
// log("lang: $langEnum");
// log("preVoice: $preVoice");
// log("postVoice: $postVoice");
// log("ticketNo: $ticketNo");
//
// String patientAlpha = "";
// String patientNumeric = "";
// String clinicName = "";
//
// bool isClinicNameAdded = (ticket.queueNo != ticket.callNoStr);
//
// if (isClinicNameAdded) {
// var queueNo = "";
// var clinic = ticketNo.split(" ");
// if (clinic.length > 1) {
// clinicName = clinic[0];
// queueNo = clinic[1];
// } else {
// queueNo = ticketNo;
// }
//
// var queueNoArray = queueNo.split("-");
// if (queueNoArray.length > 2) {
// patientAlpha = "${queueNoArray[0]} .. ${queueNoArray[1]}";
// patientNumeric = queueNoArray[2];
// } else {
// patientAlpha = queueNoArray[0];
// patientNumeric = queueNoArray[1];
// }
// } else {
// var queueNoArray = ticketNo.split("-");
// if (queueNoArray.length > 2) {
// patientAlpha = "${queueNoArray[0]} .. ${queueNoArray[1]}";
// patientNumeric = queueNoArray[2];
// } else {
// patientAlpha = queueNoArray[0];
// patientNumeric = queueNoArray[1];
// }
// }
//
// patientAlpha = patientAlpha.split('').join(' .. ');
// String roomNo = "";
//
// log("I will now all this:{ $preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo } ");
//
// if (langEnum == LanguageEnum.english) {
// await textToSpeechInstance.speak("$preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo");
// return;
// }
//
// if (isNeedToBreakVoiceForArabic) {
// await textToSpeechInstance.awaitSpeakCompletion(true);
//
// isSpeechCompleted = false;
// if (preVoice.isNotEmpty) {
// await textToSpeechInstance.speak("$preVoice ");
// }
// try {
// await textToSpeechInstance.setLanguage(LanguageEnum.english.enumToString());
// } catch (e) {
// log("error setting language english: ${e.toString()}");
// }
// await textToSpeechInstance.speak("$patientAlpha .. $patientNumeric");
//
// try {
// await textToSpeechInstance.setLanguage(langEnum.enumToString());
// } catch (e) {
// log("error setting language langEnum: ${e.toString()}");
// }
//
// await textToSpeechInstance.speak("$postVoice $roomNo").whenComplete(() {
// isSpeechCompleted = true;
// });
// } else {
// await textToSpeechInstance.speak("$preVoice .. $clinicName .. $patientAlpha .. $patientNumeric .. $postVoice $roomNo");
// }
// }
@override
Future<void> speechText({
required TicketDetailsModel ticket,
@ -169,7 +51,16 @@ class TextToSpeechServiceImp implements TextToSpeechService {
textToSpeechInstance.setVolume(1.0);
}
textToSpeechInstance.setSpeechRate(0.4);
if (isAndroid14) {
if (langEnum == LanguageEnum.arabic) {
textToSpeechInstance.setSpeechRate(0.5);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
textToSpeechInstance.setPitch(0.9);
} else {
textToSpeechInstance.setSpeechRate(0.4);
}
if (langEnum == LanguageEnum.arabic) {
try {
await textToSpeechInstance.setLanguage(LanguageEnum.arabic.enumToString());
@ -191,7 +82,7 @@ class TextToSpeechServiceImp implements TextToSpeechService {
postVoice = ticket.ticketModel!.postVoiceText;
}
String roomNo = '';
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo != null && ticket.ticketModel!.roomNo!.isNotEmpty) {
if (globalConfigurationsModel.qTypeEnum != QTypeEnum.appointment && ticket.ticketModel!.roomNo.isNotEmpty) {
roomNo = ".. ${ticket.ticketModel!.roomNo.toString()}";
}
@ -199,7 +90,7 @@ class TextToSpeechServiceImp implements TextToSpeechService {
preVoice = '$preVoice..';
}
String ticketNo = ticket.ticketModel!.queueNo!.trim().toString();
String ticketNo = ticket.ticketModel!.queueNo.trim().toString();
log("areLanguagesInstalled: ${await textToSpeechInstance.areLanguagesInstalled(["en-US", "ar-SA"])}");
log("lang: $langEnum");

@ -217,9 +217,9 @@ extension XCallType on CallTypeEnum {
}
Widget getIconByCallType(double height, QTypeEnum qType, {double? width, BoxFit fit = BoxFit.contain}) {
if (this == CallTypeEnum.vitalSign) {
return const SizedBox.shrink();
}
// if (this == CallTypeEnum.vitalSign) {
// return const SizedBox.shrink();
// }
String iconPath = "";
if (this == CallTypeEnum.vitalSign) {

@ -12,6 +12,19 @@ abstract class NativeMethodChannelService {
Future<void> clearAllResources();
Future<void> smartRestart({bool forceRestart = false, bool cleanupFirst = true});
/// Schedule daily restart alarm at specified time.
/// Works on Android 14+ and older versions.
Future<void> scheduleRestartAlarm({int hour = 0, int minute = 15});
/// Cancel the scheduled restart alarm.
Future<void> cancelRestartAlarm();
/// Check if the app can schedule exact alarms (Android 12+).
Future<bool> canScheduleExactAlarms();
/// Request permission to schedule exact alarms (Android 12+).
Future<void> requestExactAlarmPermission();
}
class NativeMethodChannelServiceImp implements NativeMethodChannelService {
@ -92,4 +105,85 @@ class NativeMethodChannelServiceImp implements NativeMethodChannelService {
loggerService.logError("Primary restart failed, trying fallback methods: $primaryError");
}
}
// === NEW: Alarm Scheduling Methods for Android 14+ compatibility ===
/// Schedule daily restart alarm at specified time.
/// Default is 00:15 (12:15 AM).
/// Works on Android 14+ and older versions.
@override
Future<void> scheduleRestartAlarm({int hour = 0, int minute = 15}) async {
try {
loggerService.logInfo("Scheduling restart alarm for $hour:$minute");
// First check if we can schedule exact alarms
final canSchedule = await canScheduleExactAlarms();
if (!canSchedule) {
loggerService.logInfo("Exact alarm permission not granted. Requesting permission...");
await requestExactAlarmPermission();
}
await _platform.invokeMethod('scheduleRestartAlarm', {
'hour': hour,
'minute': minute,
});
loggerService.logInfo("Restart alarm scheduled successfully for $hour:$minute");
} catch (e) {
loggerService.logError("Error scheduling restart alarm: $e");
loggerService.logToFile(
message: "Error scheduling restart alarm: $e",
source: "scheduleRestartAlarm -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
/// Cancel the scheduled restart alarm.
@override
Future<void> cancelRestartAlarm() async {
try {
loggerService.logInfo("Cancelling restart alarm");
await _platform.invokeMethod('cancelRestartAlarm');
loggerService.logInfo("Restart alarm cancelled successfully");
} catch (e) {
loggerService.logError("Error cancelling restart alarm: $e");
loggerService.logToFile(
message: "Error cancelling restart alarm: $e",
source: "cancelRestartAlarm -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
/// Check if the app can schedule exact alarms.
/// Returns true on Android < 12 (always allowed) or if permission is granted on Android 12+.
@override
Future<bool> canScheduleExactAlarms() async {
try {
final result = await _platform.invokeMethod('canScheduleExactAlarms');
loggerService.logInfo("Can schedule exact alarms: $result");
return result ?? false;
} catch (e) {
loggerService.logError("Error checking exact alarm permission: $e");
return false;
}
}
/// Request permission to schedule exact alarms (Android 12+).
/// Opens system settings for the user to grant permission.
@override
Future<void> requestExactAlarmPermission() async {
try {
loggerService.logInfo("Requesting exact alarm permission");
await _platform.invokeMethod('requestExactAlarmPermission');
loggerService.logInfo("Exact alarm permission request initiated");
} catch (e) {
loggerService.logError("Error requesting exact alarm permission: $e");
loggerService.logToFile(
message: "Error requesting exact alarm permission: $e",
source: "requestExactAlarmPermission -> native_method_handler.dart",
type: LogTypeEnum.error,
);
}
}
}

@ -94,8 +94,8 @@ class QueuingViewModel extends ChangeNotifier {
loggerService.logToFile(message: response.toString(), source: "onHubTicketCall -> queueing_view_model.dart ", type: LogTypeEnum.data);
log("onHubTicketCall: $response");
log("isCallingInProgress: $isCallingInProgress");
if (response != null && response.isNotEmpty) {
TicketDetailsModel ticketDetailsModel = TicketDetailsModel.fromJson(response.first as Map<String, dynamic>);
addNewTicket(ticketDetailsModel);
@ -215,7 +215,8 @@ class QueuingViewModel extends ChangeNotifier {
callTypeEnum: ticketData.callTypeEnum,
);
} else {
screenConfigViewModel.acknowledgeTicket(ticketQueueID: ticketData.id ?? 0,
screenConfigViewModel.acknowledgeTicket(
ticketQueueID: ticketData.id ?? 0,
ipAddress: screenConfigViewModel.currentScreenIP,
);
}

@ -1,5 +1,6 @@
import 'dart:developer';
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
@ -179,23 +180,23 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
void updateGlobalConfigurationsModel({required var value, bool needNotify = false, bool shouldUpdateNextPrayer = false}) {
// Ensure the incoming value is the GlobalConfigurationsModel and set the Takhasusi flag based on current IP
// Ensure the incoming value is the GlobalConfigurationsModel and set the Takhasusi flag based on projectID
if (value is GlobalConfigurationsModel) {
// IPs that belong to Takhasusi main - using AppConstants
// Use projectID-based detection (projectID = 16 for Takhasusi Main)
try {
value.isFromTakhasusiMain = AppConstants.takhasusiMainBranchIps.contains(currentScreenIP);
value.isFromTakhasusiMain = value.projectID == AppConstants.projectIdTakhasusiMain;
// Debug logging for Takhasusi Main detection
log("=== TAKHASUSI MAIN CHECK ===");
log("=== TAKHASUSI MAIN CHECK (ProjectID-based) ===");
log("Current Screen IP: $currentScreenIP");
log("Is Takhasusi Main: ${value.isFromTakhasusiMain}");
log("Takhasusi Main IPs count: ${AppConstants.takhasusiMainBranchIps.length}");
log("Project ID: ${value.projectID}");
log("Is Takhasusi Main (projectID == 16): ${value.isFromTakhasusiMain}");
if (value.isFromTakhasusiMain) {
log("✅ This device will use getTurnsByOrientationForOlderVersions()");
} else {
log("✅ This device will use getTurnsByOrientation()");
log("✅ This device will use getTurnsByOrientationForNewVersions()");
}
log("============================");
log("==========================================");
} catch (_) {
value.isFromTakhasusiMain = false;
}
@ -265,7 +266,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> getWeatherDetailsFromServer() async {
int testCityKey = 297030;
WeathersWidgetModel? response = await screenDetailsRepo.getWeatherDetailsByCity(
cityId: ((globalConfigurationsModel.cityKey == null || globalConfigurationsModel.cityKey == 0) ? testCityKey : globalConfigurationsModel.cityKey).toString(),
cityId:
((globalConfigurationsModel.cityKey == null || globalConfigurationsModel.cityKey == 0) ? testCityKey : globalConfigurationsModel.cityKey)
.toString(),
);
if (response == null) {
@ -374,7 +377,7 @@ class ScreenConfigViewModel extends ChangeNotifier {
DateTime now = DateTime.now();
log("counterValue: $counter");
if (globalConfigurationsModel.id == null) {
if (globalConfigurationsModel.id == null || state == ViewState.error) {
await getGlobalConfigurationsByIP();
}
@ -507,7 +510,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future<void> getLastTimeLogsClearedFromCache() async {
lastTimeLogsCleared = await cacheService.getLastTimeLogsCleared();
if (lastTimeLogsCleared == null) {
await cacheService.setLastTimeLogsCleared(lastTimeCleared: DateTime.now().millisecondsSinceEpoch).whenComplete(() => lastTimeLogsCleared = DateTime.now());
await cacheService
.setLastTimeLogsCleared(lastTimeCleared: DateTime.now().millisecondsSinceEpoch)
.whenComplete(() => lastTimeLogsCleared = DateTime.now());
}
}
@ -605,7 +610,8 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
}
Future<void> acknowledgeTicketForAppointmentOnly({required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
Future<void> acknowledgeTicketForAppointmentOnly(
{required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicketForAppointment(
ticketId: ticketQueueID,
ipAddress: ipAddress,

@ -1,6 +1,9 @@
import 'dart:async'; // Add this import
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:hmg_qline/models/ticket_model.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/common_widgets/app_general_widgets.dart';
import 'package:hmg_qline/views/common_widgets/date_display_widget.dart';
@ -309,11 +312,16 @@ class _AppFooterState extends State<AppFooter> {
Padding(
padding: EdgeInsets.only(top: SizeConfig.getHeightMultiplier()! * 0.1),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
AppText(
AppStrings.poweredBy,
fontSize: SizeConfig.getWidthMultiplier()! * 1.5,
fontWeight: FontWeight.w400,
color: AppColors.darkGreyTextColor,
InkWell(
onTap: () {
// context.read<QueuingViewModel>().addNewTicket(TicketDetailsModel(ticketModel: MockJsonRepo.ticket));
},
child: AppText(
AppStrings.poweredBy,
fontSize: SizeConfig.getWidthMultiplier()! * 1.5,
fontWeight: FontWeight.w400,
color: AppColors.darkGreyTextColor,
),
),
AppText(
"v${screenConfigVM.currentScreenIP.replaceAll(".", "-")}(${AppConstants.currentBuildVersion})",

@ -1,19 +1,14 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:hmg_qline/config/dependency_injection.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/models/global_config_model.dart';
import 'package:hmg_qline/utilities/enums.dart';
import 'package:hmg_qline/utilities/native_method_handler.dart';
import 'package:hmg_qline/view_models/queuing_view_model.dart';
import 'package:hmg_qline/view_models/screen_config_view_model.dart';
import 'package:hmg_qline/views/common_widgets/app_general_widgets.dart';
import 'package:provider/provider.dart';
import 'package:hmg_qline/constants/app_constants.dart';
import 'package:hmg_qline/views/common_widgets/app_texts_widget.dart';
import 'package:hmg_qline/views/view_helpers/size_config.dart';
import 'package:provider/provider.dart';
class AppHeader extends StatelessWidget implements PreferredSizeWidget {
const AppHeader({super.key});
@ -34,8 +29,10 @@ class AppHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
InkWell(
onTap: () async {
final nativeMethodChannelService = getIt.get<NativeMethodChannelService>();
await nativeMethodChannelService.smartRestart(forceRestart: true, cleanupFirst: true);
onTap: () {
// getIt.get<QueuingViewModel>().triggerOOM();
},
child: engArabicTextWithSeparatorWidget(
@ -47,7 +44,7 @@ class AppHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.end,
children: [
SvgPicture.asset(
globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment ? AppAssets.hmgLogoFull : AppAssets.hmgLogoPharmacy,
(globalConfigurationsModel.qTypeEnum == QTypeEnum.appointment || globalConfigurationsModel.qTypeEnum == QTypeEnum.lab) ? AppAssets.hmgLogoFull : AppAssets.hmgLogoPharmacy,
height: SizeConfig.getHeightMultiplier() * 0.48,
),
],

@ -58,15 +58,27 @@ class PriorityTickets extends StatelessWidget {
return [_buildPrimaryTicket(context, tickets[0], isFullWidth: true)];
case 2:
return [
Expanded(child: _buildPrimaryTicket(context, tickets[0], isHalf: true)),
globalConfigurationsModel.isFromTakhasusiMain ? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2) : SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.5),
Expanded(child: _buildPrimaryTicket(context, tickets[0], isFullWidth: true)),
globalConfigurationsModel.isFromTakhasusiMain
? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2)
: SizedBox(
height: SizeConfig.getHeightMultiplier()! *
(globalConfigurationsModel.orientationTypeEnum == ScreenOrientationEnum.landscapeRight || globalConfigurationsModel.orientationTypeEnum == ScreenOrientationEnum.landscapeLeft
? 0.7
: 0.5)),
Expanded(child: _buildSecondaryTicket(context, tickets[1], isHalf: true)),
];
case 3:
case 4:
return [
Expanded(child: _buildPrimaryTicket(context, tickets[0], isHalf: true)),
globalConfigurationsModel.isFromTakhasusiMain ? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2) : SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.5),
Expanded(child: _buildPrimaryTicket(context, tickets[0], isFullWidth: true)),
globalConfigurationsModel.isFromTakhasusiMain
? SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.2)
: SizedBox(
height: SizeConfig.getHeightMultiplier()! *
(globalConfigurationsModel.orientationTypeEnum == ScreenOrientationEnum.landscapeRight || globalConfigurationsModel.orientationTypeEnum == ScreenOrientationEnum.landscapeLeft
? 0.7
: 0.5)),
Expanded(child: _buildOtherTicketsRow(context, noOfTicketsToSkipFromStart: 1, isHalf: true)),
];
@ -108,7 +120,6 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
@ -116,30 +127,32 @@ class PriorityTickets extends StatelessWidget {
globalConfigurationsModel: globalConfigurationsModel,
);
if (isHalf) {
log("isFullWidth: ${isFullWidth} and _buildPrimaryTicket");
if (isFullWidth) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.7, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2.2, // Minimum height
maxWidth: MediaQuery.of(context).size.width * 0.6, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2, // Minimum height
),
child: Transform.scale(
scale: _getTicketScale(),
scale: _getTicketScale() + 0.2,
child: primaryCallingCard,
),
),
);
}
if (isFullWidth) {
if (isHalf) {
return Center(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * 0.6, // 60% of screen width
minHeight: SizeConfig.getHeightMultiplier() * 2, // Minimum height
minHeight: SizeConfig.getHeightMultiplier() * 2.2, // Minimum height
),
child: Transform.scale(
scale: _getTicketScale() + 0.2,
scale: _getTicketScale(),
child: primaryCallingCard,
),
),
@ -157,7 +170,6 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
@ -196,7 +208,7 @@ class PriorityTickets extends StatelessWidget {
return globalConfigurationsModel.screenTypeEnum == ScreenTypeEnum.roomLevelScreen
? 1.5
: globalConfigurationsModel.isFromTakhasusiMain
? 0.8
? 0.7
: 1.2;
}

@ -140,16 +140,24 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
priorityTickets = tickets;
}
final List<Widget> children = [
Expanded(flex: 5, child: PriorityTickets(globalConfigurationsModel: globalConfigurationsModel, tickets: priorityTickets)),
if (screenOrientationEnum == ScreenOrientationEnum.portraitUp || screenOrientationEnum == ScreenOrientationEnum.portraitDown) ...[
SizedBox(width: SizeConfig.getWidthMultiplier() * 2),
],
if (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ...[
SizedBox(height: SizeConfig.getHeightMultiplier() * 0.2),
if (tickets.length > 1 && tickets.length < 6) ...[
SizedBox(height: SizeConfig.getWidthMultiplier() * 7),
] else ...[
SizedBox(height: SizeConfig.getWidthMultiplier() * 1.5),
]
],
Expanded(flex: 5, child: PriorityTickets(globalConfigurationsModel: globalConfigurationsModel, tickets: priorityTickets)),
// if (screenOrientationEnum == ScreenOrientationEnum.portraitUp || screenOrientationEnum == ScreenOrientationEnum.portraitDown) ...[
// SizedBox(width: SizeConfig.getWidthMultiplier() * 2),
// ],
// if (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ...[
// SizedBox(height: SizeConfig.getHeightMultiplier() * 0.2),
// ],
if (showThankyouMessage) ...[
showThankyouWidget(),
] else ...[
SizedBox(height: SizeConfig.getHeightMultiplier()! * 0.1),
Expanded(
flex: !globalConfigurationsModel.isRssFeedReq && (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ? 6 : 4,
// flex: !globalConfigurationsModel.isRssFeedReq && (screenOrientationEnum == ScreenOrientationEnum.landscapeLeft || screenOrientationEnum == ScreenOrientationEnum.landscapeRight) ? 6 : 4,
@ -168,7 +176,7 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
flex: 3,
flex: 4,
child: engArabicTextWithSeparatorWidget(
englishText: globalConfigurationsModel.queueNoTextEng ?? "",
arabicText: globalConfigurationsModel.queueNoTextArb ?? "",

@ -1,3 +1,5 @@
import 'dart:developer';
import 'package:blinking_text/blinking_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
@ -14,7 +16,6 @@ class QueueItemCallingCard extends StatelessWidget {
final String roomNo;
final bool blink;
final double scale;
final bool isClinicAdded;
final bool isGradientRequired;
final bool isBorderRequired;
final TextDirection textDirection;
@ -29,7 +30,6 @@ class QueueItemCallingCard extends StatelessWidget {
const QueueItemCallingCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.scale,
@ -47,16 +47,11 @@ class QueueItemCallingCard extends StatelessWidget {
this.blink = false,
});
String getFormattedTicket(String ticketNo, bool isClinicAdded) {
if (isClinicAdded) {
var formattedString = ticketNo.split(" ");
if (formattedString.length > 1) {
return "${formattedString[0]} ${formattedString[1]}";
} else {
return ticketNo;
}
}
return ticketNo;
bool shouldReduceSize(String ticketNo) {
// Use regex to check if ticket starts with exactly 3 letters followed by " W-"
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
}
@override
@ -109,8 +104,8 @@ class QueueItemCallingCard extends StatelessWidget {
top: SizeConfig.getHeightMultiplier() * 0.25,
),
child: AppText(
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 7.4,
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 6 : SizeConfig.getWidthMultiplier() * 7.4,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,

@ -11,7 +11,6 @@ import 'package:hmg_qline/views/view_helpers/size_config.dart';
class QueueItemNormalCard extends StatelessWidget {
final String ticketNo;
final String roomNo;
final bool isClinicAdded;
final TextDirection textDirection;
final String roomText;
final String roomTextAr;
@ -25,7 +24,6 @@ class QueueItemNormalCard extends StatelessWidget {
const QueueItemNormalCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.textDirection,
@ -40,16 +38,11 @@ class QueueItemNormalCard extends StatelessWidget {
this.width,
});
String getFormattedTicket(String ticketNo, bool isClinicAdded) {
if (isClinicAdded) {
var formattedString = ticketNo.split(" ");
if (formattedString.length > 1) {
return "${formattedString[0]} ${formattedString[1]}";
} else {
return ticketNo;
}
}
return ticketNo;
bool shouldReduceSize(String ticketNo) {
// Use regex to check if ticket starts with exactly 3 letters followed by " W-"
final hasClinicPrefix = RegExp(r'^[A-Za-z]{3} W-').hasMatch(ticketNo);
return hasClinicPrefix;
}
@override
@ -76,8 +69,8 @@ class QueueItemNormalCard extends StatelessWidget {
flex: 3,
child: Center(
child: AppText(
getFormattedTicket(ticketNo, isClinicAdded),
fontSize: SizeConfig.getWidthMultiplier() * 5,
ticketNo,
fontSize: shouldReduceSize(ticketNo) ? SizeConfig.getWidthMultiplier() * 2.8 : SizeConfig.getWidthMultiplier() * 5,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,
@ -88,10 +81,13 @@ class QueueItemNormalCard extends StatelessWidget {
),
Flexible(
flex: 3,
child: Row(
children: [
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35, qTypeEnum),
],
child: Padding(
padding: EdgeInsets.all(SizeConfig.getHeightMultiplier() * 0.07),
child: Row(
children: [
callTypeEnum.getIconByCallType(SizeConfig.getHeightMultiplier() * 0.35, qTypeEnum),
],
),
),
),
Flexible(

@ -150,25 +150,6 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
builder: (BuildContext context, ScreenConfigViewModel screenConfigVM, Widget? child) {
return Column(
children: [
// if (screenConfigVM.globalConfigurationsModel.screenTypeEnum != ScreenTypeEnum.roomLevelScreen) ...[
// Expanded(
// flex: getFlexForScreenTypes(screenConfigVM),
// child: Padding(
// padding: const EdgeInsets.symmetric(horizontal: 20),
// child: Row(
// children: [
// if (screenConfigVM.globalConfigurationsModel.isWeatherReq) ...[
// getWeatherWidget(screenConfigVM),
// ],
// const SizedBox(width: 20),
// if (screenConfigVM.globalConfigurationsModel.isPrayerTimeReq) ...[
// getPrayerWidget(screenConfigVM),
// ],
// ],
// ),
// ),
// )
// ],
Expanded(
flex: 10,
child: Padding(
@ -189,8 +170,8 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
return Selector<ScreenConfigViewModel, ScreenOrientationEnum>(
selector: (context, screenConfigViewModel) => screenConfigViewModel.globalConfigurationsModel.orientationTypeEnum,
builder: (BuildContext context, ScreenOrientationEnum screenOrientationEnum, Widget? child) {
//TODO: For Testing Only
//TODO: For Testing Only
log("=== ROTATEDBOX DEBUG ===");
log("ScreenOrientationEnum: $screenOrientationEnum");
log("isFromTakhasusiMain: ${globalConfigurationsModel.isFromTakhasusiMain}");

@ -45,10 +45,10 @@ packages:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.4.0"
version: "1.4.1"
clock:
dependency: transitive
description:
@ -324,26 +324,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.17"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.16.0"
version: "1.18.0"
nested:
dependency: transitive
description:
@ -625,10 +625,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
version: "0.7.11"
tuple:
dependency: transitive
description:
@ -758,5 +758,5 @@ packages:
source: hosted
version: "1.0.1"
sdks:
dart: ">=3.8.0-0 <4.0.0"
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=3.24.0"

@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Parse command line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ip)
DEVICE_IP="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
done
if [[ -z "$DEVICE_IP" ]]; then
echo "Usage: sh clear_logs.sh -ip <device_ip:port>"
exit 1
fi
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# Remove all files in the logs directory on the device
adb -s $DEVICE_IP shell rm -rf "$LOG_PATH/*"
echo "✅ Logs cleared on $DEVICE_IP"

@ -0,0 +1,86 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh clear_logs_all.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh clear_logs_all.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices and Clear Logs =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}"
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
continue
fi
echo "🧹 Clearing logs on $cleanIP..."
adb -s "$cleanIP:5555" shell rm -rf "$LOG_PATH/*"
if [[ $? -eq 0 ]]; then
echo "✅ Logs cleared on $cleanIP"
else
echo "❌ Failed to clear logs on $cleanIP"
fi
done
echo "✅ Done! Logs cleared on all devices."

@ -0,0 +1,111 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
LOG_DIR="$SCRIPT_DIR/logs"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh connect_devices.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh connect_devices.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP")
else
echo "✅ Connected to $cleanIP"
success_ips+=("$cleanIP")
fi
done
echo "✅ Connection complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi

@ -0,0 +1,206 @@
#!/usr/bin/env bash
# ===== Config =====
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
-version)
APP_VERSION="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
fi
if [[ -z "$APP_VERSION" ]]; then
echo "Usage: sh deploy_qline.sh -ipfile <device_ips.txt> -version <app_version>"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APK_PATH="$SCRIPT_DIR/apks/qline_opd_v${APP_VERSION}_prod.apk"
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Disconnect All Devices =====
echo "🔌 Disconnecting all devices..."
adb disconnect
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect and Deploy =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP (connect)")
continue
fi
echo "📱 Processing $device..."
echo "📦 Installing APK..."
if ! adb -s "$device" install -r "$APK_PATH"; then
echo "❌ Install failed on $cleanIP"
failed_ips+=("$cleanIP (install)")
continue
fi
echo "🚀 Launching app..."
if ! adb -s "$device" shell am start -n "$APP_NAME"; then
echo "❌ Launch failed on $cleanIP"
failed_ips+=("$cleanIP (launch)")
continue
fi
success_ips+=("$cleanIP")
done
echo "✅ Deployment complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi
# Calling ==> ./deploy_qline.sh -ipfile device_ips_all.txt -version 9.2
# /Users/zik/Library/Android/sdk/platform-tools
# *********************** WOMEN HEALTH (Riyadh) ***********************
# 1st Floor (OBG)
# 10.71.50.45 - v9
# 10.71.50.50 - v9
# 10.71.50.57 - v9
# 10.71.50.52 - v9
# 2nd Floor (OBG)
# 10.71.50.46 - v9
# 10.71.50.47 - v9
# 10.71.50.48 - v9
# 10.71.50.49 - v9
# 6th Floor (IVF)
# 10.71.50.53 - v9.3 qline_opd_v9.3_bg_fix_prod
# 10.71.50.56 - v9
# *********************** HMG-KHOBAR ***********************
# 10.90.82.236 - v8.9
# *********************** TAKHASSUSI ***********************
# (G-FLOOR) ORTHOPEDIC
# 10.70.194.105 - v9.2
# 10.70.194.87 - v9.2
# *********************** SUWAIDI ***********************
# 10.80.31.37 - v9.3 (SWD Oncology & Family Med WA)
# 10.80.30.220 - v9 (SWD Oncology & Family Med WA)
# 10.80.31.67 - v9 (SWD Internal Med & Other Clinics WA)
# 10.80.31.80 - v9 (SWD Internal Med & Other Clinics WA)
# 10.80.31.100 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
# 10.80.31.103 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
# 10.80.31.104 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
# 10.80.30.234 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
# Deploy version 9 to all devices
# sh deploy_qline.sh -ipfile device_ips_all.txt -version 9
# Deploy to Khobar location only
# sh deploy_qline.sh -ipfile device_ips_khobar.txt -version 8.9
# Deploy to Takhassusi hospital
# sh deploy_qline.sh -ipfile device_ips_tak.txt -version 8.9
# Deploy to Women's Health center
# sh deploy_qline.sh -ipfile device_ips_tak_women.txt -version 9

@ -0,0 +1,59 @@
10.70.194.105
10.70.194.87
10.70.6.139
10.70.6.141
10.70.6.142
10.70.6.114
10.70.6.143
10.70.6.49
10.70.6.56
10.70.6.144
10.70.6.158
10.70.6.119
10.70.6.156
10.70.6.109
10.70.6.102
10.70.6.107
10.70.6.137
10.70.6.93
10.70.6.147
10.70.6.148
10.70.6.98
10.70.6.112
10.70.6.96
10.70.6.145
10.70.6.91
10.90.82.236
10.71.50.45
10.71.50.46
10.71.50.47
10.71.50.48
10.71.50.49
10.71.50.50
10.71.50.52
10.71.50.53
10.71.50.56
10.71.50.57
10.80.31.37
10.80.30.220
10.80.31.67
10.80.31.80
10.80.31.103
10.80.31.100
10.80.31.104
10.80.30.234
10.80.30.227
10.80.32.31
10.80.35.165
10.80.35.212
10.80.35.218
10.80.31.28
10.80.35.211
10.80.36.111
10.80.32.16
10.80.35.219
10.80.36.114
10.80.36.135
10.80.36.138
10.80.36.132
10.80.36.139

@ -0,0 +1,33 @@
10.80.31.37
10.80.30.220
10.80.31.67
10.80.31.80
10.80.31.103
10.80.31.100
10.80.31.104
10.80.30.234
10.80.30.227
10.80.32.31
10.80.35.165
10.80.35.212
10.80.35.218
10.80.31.28
10.80.35.211
10.80.36.111
10.80.32.16
10.80.35.219
10.80.36.114
10.80.36.135
10.80.36.138
10.80.36.132
10.80.36.139
10.80.26.74
10.80.26.78
10.80.35.108
10.80.36.145
10.80.20.106
10.80.20.110
10.80.31.148

@ -0,0 +1,30 @@
10.70.6.139
10.70.6.141
10.70.6.142
10.70.6.114
10.70.6.143
10.70.6.49
10.70.6.56
10.70.6.144
10.70.6.158
10.70.6.119
10.70.6.156
10.70.6.109
10.70.6.102
10.70.6.107
10.70.6.137
10.70.6.93
10.70.6.147
10.70.6.148
10.70.6.98
10.70.6.112
10.70.6.96
10.70.6.145
10.70.6.91
10.70.6.157
10.70.6.159
10.70.6.128
10.70.6.123
10.70.6.132
10.70.6.198

@ -0,0 +1,11 @@
10.71.50.45
10.71.50.46
10.71.50.47
10.71.50.48
10.71.50.49
10.71.50.50
10.71.50.52
10.71.50.53
10.71.50.56
10.71.50.57

@ -0,0 +1,67 @@
#!/usr/bin/env bash
# ===== Config =====
LOG_DIR="$(pwd)/logs"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh extract_all_logs.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh extract_all_logs.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Extract Logs from All Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}"
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
continue
fi
echo "📥 Extracting logs from $cleanIP..."
adb -s "$cleanIP:5555" shell ls "$LOG_PATH"
adb -s "$cleanIP:5555" pull "$LOG_PATH" "$LOG_DIR"
done
echo "✅ Done! Logs extracted from all devices."

@ -0,0 +1,70 @@
#!/usr/bin/env bash
# ===== Config =====
LOG_DIR="$(pwd)/logs"
LOG_PATH="/storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ip)
DEVICE_IP="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh extract_logs.sh -ip <device_ip:port>"
exit 1
;;
esac
done
if [[ -z "$DEVICE_IP" ]]; then
echo "Usage: sh extract_logs.sh -ip <device_ip:port>"
exit 1
fi
# ===== Create Local Log Directory =====
mkdir -p "$LOG_DIR"
# ===== Connect to Device =====
cleanIP="${DEVICE_IP%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
exit 1
fi
# ===== Check if Device is Available =====
if ! adb -s "$DEVICE_IP" get-state >/dev/null 2>&1; then
echo "❌ Device $DEVICE_IP is not available via ADB"
exit 1
fi
# ===== Check if Log Directory Exists on Device =====
echo "📁 Checking if log directory exists on device..."
if ! adb -s "$DEVICE_IP" shell "test -d '$LOG_PATH'" 2>/dev/null; then
echo "❌ Log directory not found on device: $LOG_PATH"
echo "💡 Available directories in Android/data/com.example.hmg_qline.hmg_qline/files/:"
adb -s "$DEVICE_IP" shell "ls -la /storage/emulated/0/Android/data/com.example.hmg_qline.hmg_qline/files/" 2>/dev/null || echo "❌ Could not list files directory"
exit 1
fi
# ===== List Log Files =====
echo "📋 Listing log files on device..."
adb -s "$DEVICE_IP" shell "ls -la '$LOG_PATH'"
# ===== Extract Logs =====
echo "📥 Extracting logs from $DEVICE_IP..."
if adb -s "$DEVICE_IP" pull "$LOG_PATH" "$LOG_DIR"; then
echo "✅ Logs extracted successfully from $DEVICE_IP to $LOG_DIR"
else
echo "❌ Failed to extract logs from $DEVICE_IP"
exit 1
fi

@ -0,0 +1,111 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_PACKAGE="com.example.hmg_qline.hmg_qline"
TIMEOUT_SECONDS=5
run_with_timeout() {
perl -e 'alarm $ARGV[0]; exec @ARGV[1..$#ARGV]' "$TIMEOUT_SECONDS" "$@"
}
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh kill_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh kill_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check =====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect and Kill App =====
success_ips=()
failed_ips=()
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
device="$cleanIP:5555"
echo "🔌 Connecting to $cleanIP..."
output=$(run_with_timeout adb connect "$device" 2>&1)
status=$?
if [[ $status -ne 0 || "$output" != *connected* ]]; then
echo "❌ Failed to connect to $cleanIP: $output"
failed_ips+=("$cleanIP (connect)")
continue
fi
echo "📱 Processing $device..."
echo "❌ Force-stopping app..."
if ! adb -s "$device" shell am force-stop "$APP_PACKAGE"; then
echo "❌ Force-stop failed on $cleanIP"
failed_ips+=("$cleanIP (kill)")
continue
fi
success_ips+=("$cleanIP")
done
echo "✅ Kill operation complete."
echo "\n====== Summary ======"
echo "✅ Success (${#success_ips[@]}):"
if [[ ${#success_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${success_ips[@]}"
else
echo " - None"
fi
echo "❌ Failed (${#failed_ips[@]}):"
if [[ ${#failed_ips[@]} -gt 0 ]]; then
printf ' - %s\n' "${failed_ips[@]}"
else
echo " - None"
fi

@ -0,0 +1,71 @@
*********************** WOMEN HEALTH (Riyadh) ***********************
1st Floor (OBG)
10.71.50.45 - v9
10.71.50.50 - v9
10.71.50.57 - v9 v9.3 qline_opd_v9.3_bg_fix_prod
10.71.50.52 - v9
2nd Floor (OBG)
10.71.50.46 - v9
10.71.50.47 - v9
10.71.50.48 - v9
10.71.50.49 - v9
6th Floor (IVF)
10.71.50.53 - v9.3 qline_opd_v9.3_bg_fix_prod
10.71.50.56 - v9
*********************** HMG-KHOBAR ***********************
10.90.82.236 - v8.9
*********************** TAKHASSUSI ***********************
(G-FLOOR) ORTHOPEDIC
10.70.194.105 - v8.9
10.70.194.87 - v8.9
*********************** SUWAIDI ***********************
10.80.31.37 - v9.3 (SWD Oncology & Family Med WA) -> qline_opd_v9.3_android_14.apk
10.80.30.220 - v9 (SWD Oncology & Family Med WA) -> qline_opd_v9.3_android_14.apk
10.80.31.67 - v9 (SWD Internal Med & Other Clinics WA) -> qline_opd_v9.3_android_14.apk
10.80.31.80 - v9 (SWD Internal Med & Other Clinics WA) -> qline_opd_v9.3_android_14.apk
10.80.31.103 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.31.100 - v9.3 (SWD PEDIATRIC & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.31.104 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.30.234 - v9.3 (SWD OB-GYN & Other Clinics Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.30.227 - v9.3 (SWD CARDIOLOGY & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.32.31 - v9.3 (SWD CARDIOLOGY & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.165 - v9.3 (SWD Urology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.212 - v9.3 (SWD Urology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.218 - v9.3 (SWD General Surgery & Other Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.31.28 - v9.3 (SWD General Surgery & Other Waiting Area) -> qline_opd_v9.3_android_14.apk
10.80.35.211 - v9.3 (SWD ENT & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.111 - v9.3 (SWD ENT & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.32.16 - v9.3 (SWD PULMONARY and Other WA) -> qline_opd_v9.3_android_14.apk
10.80.35.219 - v9.3 (SWD Ophthalmology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.114 - v9.3 (SWD Ophthalmology & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.135 - v9.3 (SWD Dermatology / Wound Care / Plastic Surgery & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.138 - v9.3 (SWD Dermatology / Wound Care / Plastic Surgery & Other WA) -> qline_opd_v9.3_android_14.apk
10.80.36.132 - v9.3 (SWD Gastroenterology / NEUROLOGY / DIABETIC EDUCATOR / GP Departments WA ) -> qline_opd_v9.3_android_14.apk
10.80.36.139 - v9.3 (SWD Gastroenterology / NEUROLOGY / DIABETIC EDUCATOR / GP Departments WA ) -> qline_opd_v9.3_android_14.apk
10.80.26.78 - v9.3 (SWD ORTHOPEDIC / SPINAL SURGERY / NEURO SURGERY/ PAIN CLINIC / Departments) -> qline_opd_v9.3_android_14.apk
10.80.26.74 - v9.3 (SWD ORTHOPEDIC / SPINAL SURGERY / NEURO SURGERY/ PAIN CLINIC / Departments) -> qline_opd_v9.3_android_14.apk
10.80.35.108 - v9.3 SWD Ophthalmology & Other WA -> qline_opd_v9.3_android_14.apk
10.80.36.145 - v9.3 SWD Dental & Other WA -> qline_opd_v9.3_android_14.apk
10.80.20.110 - v9.3 SWD Physiotherapy Male WA -> qline_opd_v9.3_android_14.apk
10.80.20.106 - v9.3 SWD Physiotherapy Female WA -> qline_opd_v9.3_android_14.apk
10.80.31.148 - v9.3 SWD Oncology & Family Med WA -> qline_opd_v9.3_android_14.apk

@ -0,0 +1,99 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_NAME="com.example.hmg_qline.hmg_qline/.MainActivity"
LOG_DIR="$SCRIPT_DIR/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh restart_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh restart_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check ====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
fi
done
# ===== Get Connected Devices =====
devices=$(adb devices | awk 'NR>1 && $2=="device" {print $1}')
for device in $devices; do
echo "📱 Processing $device..."
echo "❌ Closing app if running..."
adb -s "$device" shell am force-stop "${APP_NAME%%/*}"
echo "🚀 Launching app..."
adb -s "$device" shell am start -n "$APP_NAME"
log_file="$LOG_DIR/logs_${device//[^a-zA-Z0-9]/_}.txt"
echo "📝 Logging to $log_file"
adb -s "$device" logcat -s flutter > "$log_file" &
done
echo "✅ Done! Logs are saved to $LOG_DIR"

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# ===== Config =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
APP_PACKAGE="com.example.hmg_qline.hmg_qline"
LOG_DIR="$SCRIPT_DIR/logs"
# ===== Parse Arguments =====
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
-ipfile)
IP_FILE="$2"
shift # past argument
shift # past value
;;
*)
echo "Unknown option: $1"
echo "Usage: sh uninstall_qline.sh -ipfile <device_ips.txt>"
exit 1
;;
esac
done
if [[ -z "$IP_FILE" ]]; then
echo "Usage: sh uninstall_qline.sh -ipfile <device_ips.txt>"
exit 1
fi
# ===== Wi-Fi Check ====
wifi_interface=$(networksetup -listallhardwareports | \
awk '/Wi-Fi|AirPort/{getline; print $2}')
current_ssid=$(networksetup -getairportnetwork "$wifi_interface" 2>/dev/null | \
awk -F': ' '{print $2}' | tr '[:upper:]' '[:lower:]')
expected_ssid="cloud solutions"
if [[ "$current_ssid" != "$expected_ssid" ]]; then
echo "❌ Not connected to required Wi-Fi network: $expected_ssid"
echo "🔌 Current network: ${current_ssid:-Not connected}"
exit 1
fi
echo "✅ Connected to correct Wi-Fi: Cloud Solutions"
# Create log dir
mkdir -p "$LOG_DIR"
# ===== Read IPs =====
if [[ ! -f "$IP_FILE" ]]; then
echo "❌ IP file not found at $IP_FILE"
exit 1
fi
ipList=()
while IFS= read -r line; do
[[ -n "$line" ]] && ipList+=("$line")
done < "$IP_FILE"
if [[ ${#ipList[@]} -eq 0 ]]; then
echo "❌ No IPs found in file"
exit 1
fi
echo "✅ Found ${#ipList[@]} IPs"
printf '%s\n' "${ipList[@]}"
# ===== Connect to Devices =====
for ip in "${ipList[@]}"; do
cleanIP="${ip%%:*}" # strip port if exists
echo "🔌 Connecting to $cleanIP..."
output=$(adb connect "$cleanIP:5555" 2>&1)
if [[ "$output" == *connected* ]]; then
echo "✅ Connected to $cleanIP"
else
echo "❌ Failed to connect to $cleanIP: $output"
fi
done
# ===== Get Connected Devices =====
devices=$(adb devices | awk 'NR>1 && $2=="device" {print $1}')
for device in $devices; do
echo "📱 Processing $device..."
echo "❌ Uninstalling app..."
adb -s "$device" uninstall "$APP_PACKAGE"
if [[ $? -eq 0 ]]; then
echo "✅ Successfully uninstalled $APP_PACKAGE from $device"
else
echo "❌ Failed to uninstall $APP_PACKAGE from $device"
fi
done
echo "✅ Done! Uninstallation process completed."
Loading…
Cancel
Save