diff --git a/android/app/build.gradle b/android/app/build.gradle
index c31d5ad..c4d0a62 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -45,4 +45,5 @@ flutter {
dependencies {
implementation 'androidx.lifecycle:lifecycle-service:2.8.7'
+ implementation 'androidx.core:core-ktx:1.13.1'
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index fbf5364..dd7c5c1 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,12 @@
+
+
+
+
+
+
@@ -24,14 +30,30 @@
-
+
+
+
+
+
+
+
+
+ android:exported="true"
+ android:foregroundServiceType="specialUse">
+
+
+
= 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}")
+ }
+ }
+ }
+}
+
diff --git a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootForegroundService.kt b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootForegroundService.kt
index e5d8522..915a2bc 100644
--- a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootForegroundService.kt
+++ b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootForegroundService.kt
@@ -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")
+ }
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootReceiver.kt b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootReceiver.kt
index 4c83c97..7e02b1b 100644
--- a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootReceiver.kt
+++ b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/BootReceiver.kt
@@ -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}")
}
}
}
\ No newline at end of file
diff --git a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/MainActivity.kt b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/MainActivity.kt
index 7c974e3..9a384b3 100644
--- a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/MainActivity.kt
@@ -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
@@ -41,85 +44,168 @@ class MainActivity : FlutterActivity() {
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("hour") ?: 0
+ val minute = call.argument("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(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("MainActivity", "Error during restart: ${e.message}")
- // Fallback - don't exit, just log the error
+ Log.e(TAG, "Fallback restart also failed: ${e.message}")
}
}
@@ -260,15 +346,48 @@ class MainActivity : FlutterActivity() {
}
}
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // 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)
+ }
+
+ // 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 +395,7 @@ class MainActivity : FlutterActivity() {
override fun onDestroy() {
super.onDestroy()
- Log.d("MainActivity", "Activity destroyed")
+ Log.d(TAG, "Activity destroyed")
// Final cleanup
clearAllNativeResources()
diff --git a/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/RestartAlarmReceiver.kt b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/RestartAlarmReceiver.kt
new file mode 100644
index 0000000..b12ca9d
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/hmg_qline/hmg_qline/RestartAlarmReceiver.kt
@@ -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}")
+ }
+ }
+}
+
diff --git a/android/build.gradle b/android/build.gradle
index 81e43bb..d0adc08 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- ext.kotlin_version = '1.9.10'
+ ext.kotlin_version = '2.1.0'
repositories {
google()
mavenCentral()
diff --git a/android/settings.gradle b/android/settings.gradle
index 1c66220..e3bfcfb 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -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
}
diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart
index 4e07f60..7990186 100644
--- a/lib/api/api_client.dart
+++ b/lib/api/api_client.dart
@@ -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 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") {
diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart
index 2476dec..1cc3874 100644
--- a/lib/constants/app_constants.dart
+++ b/lib/constants/app_constants.dart
@@ -8,11 +8,11 @@ bool useTestIP = false;
bool isNeedToBreakVoiceForArabic = true;
bool isSpeechCompleted = true;
+bool isAndroid14 = true;
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";
@@ -268,6 +268,13 @@ class MockJsonRepo {
id: 189805,
patientID: 4292695,
queueNo: 'W-T-4',
+ laBQGroupID: null,
+ queueNo: 'FMC W-T-4',
+ counterBatchNo: null,
+ calledBy: null,
+ calledOn: null,
+ servedOn: null,
+ patientName: null,
mobileNo: '0598544522',
patientEmail: 'munira.ali@hotmail.com',
preferredLang: 2,
@@ -279,8 +286,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,
@@ -302,6 +309,9 @@ class MockJsonRepo {
queueNoText: 'رقم الانتظار',
callForText: 'التوجه الى',
);
+
+
+
}
// RAW DATA:
diff --git a/lib/main.dart b/lib/main.dart
index d70cf97..8c08bb6 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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 {
@@ -56,8 +56,9 @@ class MyApp extends StatelessWidget {
log("=====================================");
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
- SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
-
+ if (!isAndroid14) {
+ SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
+ }
return MultiProvider(
providers: [
ChangeNotifierProvider(
diff --git a/lib/models/generic_response_model.dart b/lib/models/generic_response_model.dart
index e6c1827..d65fbe9 100644
--- a/lib/models/generic_response_model.dart
+++ b/lib/models/generic_response_model.dart
@@ -1,3 +1,5 @@
+import 'dart:developer';
+
class GenericRespModel {
GenericRespModel({
this.data,
@@ -12,6 +14,7 @@ class GenericRespModel {
String? message;
factory GenericRespModel.fromJson(Map json) {
+ log("jsonjsonjosn: $json");
if (json.containsKey('StatusMessage')) {
if ((json['StatusMessage'] as String).contains('Internal server error')) {
// Utils.showToast("${json['StatusMessage']}");
diff --git a/lib/models/global_config_model.dart b/lib/models/global_config_model.dart
index dfd0b59..2a9f8f7 100644
--- a/lib/models/global_config_model.dart
+++ b/lib/models/global_config_model.dart
@@ -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;
@@ -172,6 +175,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,
@@ -268,6 +273,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;
@@ -280,7 +287,8 @@ class GlobalConfigurationsModel {
kioskQueueList = [];
}
if (json['kioskConfig'] != null) {
- kioskLanguageConfigList = List.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
+ kioskLanguageConfigList =
+ List.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
} else {
kioskLanguageConfigList = [];
}
diff --git a/lib/repositories/screen_details_repo.dart b/lib/repositories/screen_details_repo.dart
index 86e6bfb..0a3db26 100644
--- a/lib/repositories/screen_details_repo.dart
+++ b/lib/repositories/screen_details_repo.dart
@@ -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 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 = List.generate(genericModel.data.length, (index) => GlobalConfigurationsModel.fromJson(json: genericModel.data[index]));
+ List 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 = List.generate(genericRespModel.data.length, (index) => WeathersWidgetModel.fromJson(genericRespModel.data[index]));
+ List 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 = List.generate(genericRespModel.data.length, (index) => PrayersWidgetModel.fromJson(genericRespModel.data[index]));
+ List 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 = 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 acknowledgeTicketForAppointment({required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
+ Future acknowledgeTicketForAppointment(
+ {required int ticketId, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
try {
var params = {
"id": ticketId.toString(),
diff --git a/lib/services/text_to_speech_service.dart b/lib/services/text_to_speech_service.dart
index 868dafb..302e821 100644
--- a/lib/services/text_to_speech_service.dart
+++ b/lib/services/text_to_speech_service.dart
@@ -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 speechText({
@@ -15,8 +15,6 @@ abstract class TextToSpeechService {
bool isMute = false,
});
- // Future speechTextTest(TicketData ticket);
-
void listenToTextToSpeechEvents({required Function() onVoiceCompleted});
}
@@ -30,122 +28,6 @@ class TextToSpeechServiceImp implements TextToSpeechService {
double pitch = 0.6;
Map arabicVoice = {"name": "ar-xa-x-ard-local", "locale": "ar"};
- @override
- // Future 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 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,9 @@ 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 != null &&
+ ticket.ticketModel!.roomNo!.isNotEmpty) {
roomNo = ".. ${ticket.ticketModel!.roomNo.toString()}";
}
diff --git a/lib/utilities/native_method_handler.dart b/lib/utilities/native_method_handler.dart
index c87c85b..a3f2535 100644
--- a/lib/utilities/native_method_handler.dart
+++ b/lib/utilities/native_method_handler.dart
@@ -12,6 +12,19 @@ abstract class NativeMethodChannelService {
Future clearAllResources();
Future smartRestart({bool forceRestart = false, bool cleanupFirst = true});
+
+ /// Schedule daily restart alarm at specified time.
+ /// Works on Android 14+ and older versions.
+ Future scheduleRestartAlarm({int hour = 0, int minute = 15});
+
+ /// Cancel the scheduled restart alarm.
+ Future cancelRestartAlarm();
+
+ /// Check if the app can schedule exact alarms (Android 12+).
+ Future canScheduleExactAlarms();
+
+ /// Request permission to schedule exact alarms (Android 12+).
+ Future 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 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 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 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 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,
+ );
+ }
+ }
}
diff --git a/lib/view_models/queuing_view_model.dart b/lib/view_models/queuing_view_model.dart
index 99a7611..9ec5e82 100644
--- a/lib/view_models/queuing_view_model.dart
+++ b/lib/view_models/queuing_view_model.dart
@@ -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);
addNewTicket(ticketDetailsModel);
diff --git a/lib/view_models/screen_config_view_model.dart b/lib/view_models/screen_config_view_model.dart
index ecbf5d6..ae134b9 100644
--- a/lib/view_models/screen_config_view_model.dart
+++ b/lib/view_models/screen_config_view_model.dart
@@ -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';
@@ -265,7 +266,9 @@ class ScreenConfigViewModel extends ChangeNotifier {
Future 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 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 acknowledgeTicketForAppointmentOnly({required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
+ Future acknowledgeTicketForAppointmentOnly(
+ {required int ticketQueueID, required String ipAddress, required CallTypeEnum callTypeEnum}) async {
GenericRespModel? response = await screenDetailsRepo.acknowledgeTicketForAppointment(
ticketId: ticketQueueID,
ipAddress: ipAddress,
diff --git a/lib/views/common_widgets/app_footer.dart b/lib/views/common_widgets/app_footer.dart
index b073f7e..c499956 100644
--- a/lib/views/common_widgets/app_footer.dart
+++ b/lib/views/common_widgets/app_footer.dart
@@ -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 {
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().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})",
diff --git a/lib/views/common_widgets/app_header.dart b/lib/views/common_widgets/app_header.dart
index 68bd294..9f318f5 100644
--- a/lib/views/common_widgets/app_header.dart
+++ b/lib/views/common_widgets/app_header.dart
@@ -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();
+ await nativeMethodChannelService.smartRestart(forceRestart: true, cleanupFirst: true);
- onTap: () {
// getIt.get().triggerOOM();
},
child: engArabicTextWithSeparatorWidget(
diff --git a/lib/views/main_queue_screen/components/priority_tickets.dart b/lib/views/main_queue_screen/components/priority_tickets.dart
index 2c6abab..d1c06bf 100644
--- a/lib/views/main_queue_screen/components/priority_tickets.dart
+++ b/lib/views/main_queue_screen/components/priority_tickets.dart
@@ -108,6 +108,7 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
+ callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
textDirection: globalConfigurationsModel.textDirection,
@@ -159,6 +160,7 @@ class PriorityTickets extends StatelessWidget {
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.none,
+ callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
langTypeEnum: globalConfigurationsModel.screenLanguageEnum,
diff --git a/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart b/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart
index 87cc417..ca66345 100644
--- a/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart
+++ b/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart
@@ -168,7 +168,7 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
- flex: 3,
+ flex: 4,
child: engArabicTextWithSeparatorWidget(
englishText: globalConfigurationsModel.queueNoTextEng ?? "",
arabicText: globalConfigurationsModel.queueNoTextArb ?? "",
diff --git a/lib/views/main_queue_screen/components/ticket_item_calling_card.dart b/lib/views/main_queue_screen/components/ticket_item_calling_card.dart
index 4db3626..0224cb5 100644
--- a/lib/views/main_queue_screen/components/ticket_item_calling_card.dart
+++ b/lib/views/main_queue_screen/components/ticket_item_calling_card.dart
@@ -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,
diff --git a/lib/views/main_queue_screen/components/ticket_item_normal_card.dart b/lib/views/main_queue_screen/components/ticket_item_normal_card.dart
index b0c8fbc..231316f 100644
--- a/lib/views/main_queue_screen/components/ticket_item_normal_card.dart
+++ b/lib/views/main_queue_screen/components/ticket_item_normal_card.dart
@@ -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.5 : SizeConfig.getWidthMultiplier() * 5,
letterSpacing: -1,
fontHeight: 0.5,
color: AppColors.greyTextColor,