Compare commits

...

3 Commits

Author SHA1 Message Date
Faiz Hashmi aa81f40118 fixed size issue on calling cards 6 days ago
faizatflutter 856395f273 android 14 stable version 1 month ago
faizatflutter d9c0ce5180 android 14 testing started. 1 month ago

@ -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")
// 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)
putExtra("launched_from_boot", source == "boot_completed")
putExtra("launched_from_scheduled_restart", source == "scheduled_restart")
}
startActivity(intent)
stopSelf() // Stop the service after initialization
Log.d(TAG, "MainActivity launched successfully")
} catch (e: Exception) {
Log.e(TAG, "Error launching MainActivity: ${e.message}")
}
}
/**
* 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
@ -13,85 +16,168 @@ import java.io.File
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.hmg_qline/foreground"
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()
}
}
}
}
/**
* Schedule daily restart alarm at specified time.
* Compatible with Android 14+ and older versions.
*/
private fun scheduleRestartAlarm(hour: Int, minute: Int) {
try {
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}")
}
}
private fun restartApplication() {
try {
Log.d("MainActivity", "Initiating app restart")
Log.d(TAG, "Initiating app restart")
// Clear resources before restart
clearAllNativeResources()
// Get the launch intent
val intent = packageManager.getLaunchIntentForPackage(packageName)
// Create restart intent
val intent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
if (intent != null) {
// 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)
}
if (intent != null) {
// Use a shorter delay for faster restart
// 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}")
}
}
@ -237,25 +323,43 @@ class MainActivity : FlutterActivity() {
// Log if app was restarted
if (intent.getBooleanExtra("restarted", false)) {
Log.d("MainActivity", "App restarted successfully")
Log.d(TAG, "App restarted successfully")
}
// Log if launched from boot
if (intent.getBooleanExtra("launched_from_boot", false)) {
Log.d("MainActivity", "App launched from boot")
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()
@ -263,7 +367,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()

@ -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
}

@ -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}");
}
@ -97,6 +101,7 @@ class ApiClientImp implements ApiClient {
loggerService.logInfo("------Response------");
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 = 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";
@ -167,7 +167,7 @@ class AppConstants {
static String apiKey = 'EE17D21C7943485D9780223CCE55DCE5';
static String testIP = '12.4.5.1'; // projectID.QlineType.ScreenType.AnyNumber (1 to 10)
static int thresholdForListUI = 5;
static double currentBuildVersion = 9.2;
static double currentBuildVersion = 9.3;
static double clearLogsHoursThreshold = 48;
// Maximum log file size in bytes before rotation/clearing. Default 2 MB.
static int maxLogFileSizeBytes = 2 * 1024 * 1024;
@ -236,7 +236,7 @@ class MockJsonRepo {
id: 189805,
patientID: 4292695,
laBQGroupID: null,
queueNo: 'W-T-4',
queueNo: 'FMC W-T-4',
counterBatchNo: null,
calledBy: null,
calledOn: null,
@ -257,8 +257,8 @@ class MockJsonRepo {
createdOn: DateTime.parse('2025-08-18 15:06:07.363'),
doctorNameN: null,
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,
@ -281,6 +281,9 @@ class MockJsonRepo {
queueNoText: 'رقم الانتظار',
callForText: 'التوجه الى',
);
}
// RAW DATA:

@ -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 {
@ -45,7 +45,9 @@ class MyApp extends StatelessWidget {
return OrientationBuilder(builder: (context, orientation) {
SizeConfig().init(constraints, orientation);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
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';
@ -68,6 +69,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;
@ -163,6 +166,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,
@ -258,6 +263,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'];
@ -270,22 +277,23 @@ class GlobalConfigurationsModel {
kioskQueueList = [];
}
if (json['kioskConfig'] != null) {
kioskLanguageConfigList = List<KioskLanguageConfigModel>.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
kioskLanguageConfigList =
List<KioskLanguageConfigModel>.from(json['kioskConfig'].map((kioskQueueJson) => KioskLanguageConfigModel.fromJson(kioskQueueJson)));
} else {
kioskLanguageConfigList = [];
}
// Default to false; actual value (based on device IP) is set in ViewModel after loading config
isFromTakhasusiMain = false;
vitalSignTextEng = json['vitalSignText'];
doctorTextEng = json['doctorText'];
procedureTextEng = json['procedureText'];
vaccinationTextEng = json['vaccinationText'];
nebulizationTextEng = json['nebulizationText'];
callForVitalSignTextEng = json['callForVitalSignText'];
callForDoctorTextEng = json['callForDoctorText'];
callForProcedureTextEng = json['callForProcedureText'];
callForVaccinationTextEng = json['callForVaccinationText'];
callForNebulizationTextEng = json['callForNebulizationText'];
vitalSignTextEng = json['vitalSignText'] ?? "Vital Sign";
doctorTextEng = json['doctorText'] ?? "Doctor";
procedureTextEng = json['procedureText'] ?? "Procedure";
vaccinationTextEng = json['vaccinationText'] ?? "Vaccination";
nebulizationTextEng = json['nebulizationText'] ?? "Nebulization";
callForVitalSignTextEng = json['callForVitalSignText'] ?? "Call for Vital Sign";
callForDoctorTextEng = json['callForDoctorText'] ?? "Call for Doctor";
callForProcedureTextEng = json['callForProcedureText'] ?? "Call for Procedure";
callForVaccinationTextEng = json['callForVaccinationText'] ?? "Call for Vaccination";
callForNebulizationTextEng = json['callForNebulizationText'] ?? "Call for Nebulization";
vitalSignTextArb = json['vitalSignTextAr'] ?? "غرفة العلامات الحيوية";
doctorTextArb = json['doctorTextAr'] ?? " غرفة الطبيب";
@ -301,6 +309,6 @@ class GlobalConfigurationsModel {
@override
String toString() {
return 'GlobalConfigurationsModel{id: $id, isFromTakhasusiMain: $isFromTakhasusiMain, configType: $configType, description: $description, counterStart: $counterStart, counterEnd: $counterEnd, concurrentCallDelaySec: $concurrentCallDelaySec, voiceType: $voiceType, voiceTypeText: $voiceTypeText, screenLanguageEnum: $screenLanguageEnum, screenLanguageText: $screenLanguageText, textDirection: $textDirection, voiceLanguageEnum: $voiceLanguageEnum, voiceLanguageText: $voiceLanguageText, screenMaxDisplayPatients: $screenMaxDisplayPatients, isNotiReq: $isNotiReq, prioritySMS: $prioritySMS, priorityWhatsApp: $priorityWhatsApp, priorityEmail: $priorityEmail, ticketNoText: $ticketNoText, postVoiceText: $postVoiceText, roomText: $roomTextEng, roomNo: $roomNo, isRoomNoRequired: $isRoomNoRequired, counterText: $counterTextEng, queueNoText: $queueNoTextEng, callForText: $callForTextEng, currentServeTextArb: $currentServeTextArb,, currentServeTextEng: $currentServeTextEng, maxText: $maxText, minText: $minText, nextPrayerTextEng: $nextPrayerTextEng, nextPrayerTextArb: $nextPrayerTextArb, weatherText: $weatherText, fajarText: $fajarTextEng, dhuhrText: $dhuhrTextEng, asarText: $asarTextEng, maghribText: $maghribTextEng, ishaText: $ishaTextEng, isActive: $isActive, createdBy: $createdBy, createdOn: $createdOn, editedBy: $editedBy, editedOn: $editedOn, isToneReq: $isToneReq, isVoiceReq: $isVoiceReq, orientationTypeEnum: $orientationTypeEnum, isTurnOn: $isTurnOn, waitingAreaType: $waitingAreaType, gender: $gender, isWeatherReq: $isWeatherReq, isPrayerTimeReq: $isPrayerTimeReq, isRssFeedReq: $isRssFeedReq, qTypeEnum: $qTypeEnum, screenTypeEnum: $screenTypeEnum, projectID: $projectID, projectLatitude: $projectLatitude, projectLongitude: $projectLongitude, cityKey: $cityKey, kioskQueueList: $kioskQueueList, kioskLanguageConfigList: $kioskLanguageConfigList, vitalSignText: $vitalSignTextEng, doctorText: $doctorTextEng, procedureText: $procedureTextEng, vaccinationText: $vaccinationTextEng, nebulizationText: $nebulizationTextEng, callForVitalSignText: $callForVitalSignTextEng, callForDoctorText: $callForDoctorTextEng, callForProcedureText: $callForProcedureTextEng, callForVaccinationText: $callForVaccinationTextEng, callForNebulizationText: $callForNebulizationTextEng, vitalSignTextArb: $vitalSignTextArb, doctorTextArb: $doctorTextArb, procedureTextArb: $procedureTextArb, vaccinationTextArb: $vaccinationTextArb, nebulizationTextArb: $nebulizationTextArb, callForVitalSignTextArb: $callForVitalSignTextArb, callForDoctorTextArb: $callForDoctorTextArb, callForProcedureTextArb: $callForProcedureTextArb, callForVaccinationTextArb: $callForVaccinationTextArb, callForNebulizationTextArb: $callForNebulizationTextArb}';
return 'GlobalConfigurationsModel{id: $id, isFromTakhasusiMain: $isFromTakhasusiMain, configType: $configType, description: $description, counterStart: $counterStart, counterEnd: $counterEnd, concurrentCallDelaySec: $concurrentCallDelaySec, voiceType: $voiceType, voiceTypeText: $voiceTypeText, screenLanguageEnum: $screenLanguageEnum, screenLanguageText: $screenLanguageText, textDirection: $textDirection, voiceLanguageEnum: $voiceLanguageEnum, voiceLanguageText: $voiceLanguageText, screenMaxDisplayPatients: $screenMaxDisplayPatients, isNotiReq: $isNotiReq, prioritySMS: $prioritySMS, priorityWhatsApp: $priorityWhatsApp, priorityEmail: $priorityEmail, ticketNoText: $ticketNoText, postVoiceText: $postVoiceText, roomText: $roomTextEng, roomNo: $roomNo, isRoomNoRequired: $isRoomNoRequired, counterText: $counterTextEng, queueNoText: $queueNoTextEng, callForText: $callForTextEng, currentServeTextArb: $currentServeTextArb,, currentServeTextEng: $currentServeTextEng, maxText: $maxText, minText: $minText, nextPrayerTextEng: $nextPrayerTextEng, nextPrayerTextArb: $nextPrayerTextArb, weatherText: $weatherText, fajarText: $fajarTextEng, dhuhrText: $dhuhrTextEng, asarText: $asarTextEng, maghribText: $maghribTextEng, ishaText: $ishaTextEng, isActive: $isActive, createdBy: $createdBy, createdOn: $createdOn, editedBy: $editedBy, editedOn: $editedOn, isToneReq: $isToneReq, isVoiceReq: $isVoiceReq, orientationTypeEnum: $orientationTypeEnum, isTurnOn: $isTurnOn, waitingAreaType: $waitingAreaType, gender: $gender, isWeatherReq: $isWeatherReq, isPrayerTimeReq: $isPrayerTimeReq, isRssFeedReq: $isRssFeedReq, globalClinicPrefixReq: $globalClinicPrefixReq, clinicPrefixReq: $clinicPrefixReq, qTypeEnum: $qTypeEnum, screenTypeEnum: $screenTypeEnum, projectID: $projectID, projectLatitude: $projectLatitude, projectLongitude: $projectLongitude, cityKey: $cityKey, kioskQueueList: $kioskQueueList, kioskLanguageConfigList: $kioskLanguageConfigList, vitalSignText: $vitalSignTextEng, doctorText: $doctorTextEng, procedureText: $procedureTextEng, vaccinationText: $vaccinationTextEng, nebulizationText: $nebulizationTextEng, callForVitalSignText: $callForVitalSignTextEng, callForDoctorText: $callForDoctorTextEng, callForProcedureText: $callForProcedureTextEng, callForVaccinationText: $callForVaccinationTextEng, callForNebulizationText: $callForNebulizationTextEng, vitalSignTextArb: $vitalSignTextArb, doctorTextArb: $doctorTextArb, procedureTextArb: $procedureTextArb, vaccinationTextArb: $vaccinationTextArb, nebulizationTextArb: $nebulizationTextArb, callForVitalSignTextArb: $callForVitalSignTextArb, callForDoctorTextArb: $callForDoctorTextArb, callForProcedureTextArb: $callForProcedureTextArb, callForVaccinationTextArb: $callForVaccinationTextArb, callForNebulizationTextArb: $callForNebulizationTextArb}';
}
}

@ -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(),

@ -95,7 +95,7 @@ class LoggerServiceImp implements LoggerService {
try {
ScreenConfigViewModel screenConfigViewModel = getIt.get<ScreenConfigViewModel>();
final timestamp = DateFormat('yyyy-MM-dd hh:mm:ss a').format(DateTime.now());
final sep = '==================== CRASH ====================';
const sep = '==================== CRASH ====================';
final contextStr = context ?? 'No context provided';
final errorMsg = error.toString();
final stack = stackTrace?.toString() ?? 'No stack trace available';

@ -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);
}
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()}";
}

@ -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);

@ -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';
@ -49,16 +50,19 @@ class ScreenConfigViewModel extends ChangeNotifier {
}
Future<void> onAppResumed() async {
loggerService.logToFile(message: "[didChangeAppLifecycleState] : [onAppResumed]", source: "onAppResumed -> screen_config_view_model.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppResumed]", source: "onAppResumed -> screen_config_view_model.dart", type: LogTypeEnum.data);
}
Future<void> onAppPaused() async {
loggerService.logToFile(message: "[didChangeAppLifecycleState] : [onAppPaused]", source: "onAppPaused -> screen_config_view_model.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppPaused]", source: "onAppPaused -> screen_config_view_model.dart", type: LogTypeEnum.data);
// nativeMethodChannelService.restartApp();
}
Future<void> onAppDetached() async {
loggerService.logToFile(message: "[didChangeAppLifecycleState] : [onAppDetached]", source: "onAppDetached -> screen_config_view_model.dart", type: LogTypeEnum.data);
loggerService.logToFile(
message: "[didChangeAppLifecycleState] : [onAppDetached]", source: "onAppDetached -> screen_config_view_model.dart", type: LogTypeEnum.data);
// nativeMethodChannelService.restartApp();
}
@ -229,7 +233,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) {
@ -336,7 +342,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();
}
@ -393,7 +399,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());
}
}
@ -491,7 +499,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,12 +312,17 @@ class _AppFooterState extends State<AppFooter> {
Padding(
padding: EdgeInsets.only(top: SizeConfig.getHeightMultiplier()! * 0.1),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
AppText(
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})",
fontSize: SizeConfig.getWidthMultiplier()! * 1,

@ -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(

@ -103,7 +103,6 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,
@ -150,7 +149,6 @@ class PriorityTickets extends StatelessWidget {
roomNo: ticket.ticketModel?.roomNo ?? '',
roomText: _getRoomText(),
roomTextAr: _getRoomTextAr(),
isClinicAdded: false,
callTypeEnum: ticket.ticketModel?.callTypeEnum ?? CallTypeEnum.vitalSign,
textDirection: globalConfigurationsModel.textDirection,
screenTypeEnum: globalConfigurationsModel.screenTypeEnum,

@ -86,7 +86,7 @@ class PriorityTicketsWithSideSection extends StatelessWidget {
SizedBox(width: SizeConfig.getWidthMultiplier()),
if (callMessageEng.isNotEmpty) ...[
Expanded(
flex: 2,
flex: 3,
child: AppText(
"($callMessageEng)",
color: ticketModel.callTypeEnum.getColorByCallType(),
@ -170,7 +170,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;
@ -28,7 +29,6 @@ class QueueItemCallingCard extends StatelessWidget {
const QueueItemCallingCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.scale,
@ -45,16 +45,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
@ -107,8 +102,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;
@ -24,7 +23,6 @@ class QueueItemNormalCard extends StatelessWidget {
const QueueItemNormalCard({
super.key,
required this.isClinicAdded,
required this.ticketNo,
required this.roomNo,
required this.textDirection,
@ -38,16 +36,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
@ -73,8 +66,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,

@ -192,7 +192,9 @@ class _MainQueueScreenState extends State<MainQueueScreen> {
// context.read<ScreenConfigViewModel>().createAutoTickets(numOfTicketsToCreate: 20);
// context.read<QueuingViewModel>().testSpeech();
return RotatedBox(
quarterTurns: globalConfigurationsModel.isFromTakhasusiMain ? screenOrientationEnum.getTurnsByOrientationForOlderVersions() : screenOrientationEnum.getTurnsByOrientation(),
quarterTurns: globalConfigurationsModel.isFromTakhasusiMain
? screenOrientationEnum.getTurnsByOrientationForOlderVersions()
: screenOrientationEnum.getTurnsByOrientation(),
child: AppScaffold(
backgroundColor: AppColors.backgroundColor,
appBar: const AppHeader(),

Loading…
Cancel
Save