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 f4b26f2..ad16a12 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 09952db..8225164 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 @@ -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("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}") } } @@ -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() 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/constants/app_constants.dart b/lib/constants/app_constants.dart index 9223e46..ae2936e 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"; @@ -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; diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart index 24cb012..6c0732a 100644 --- a/lib/services/logger_service.dart +++ b/lib/services/logger_service.dart @@ -95,7 +95,7 @@ class LoggerServiceImp implements LoggerService { try { ScreenConfigViewModel screenConfigViewModel = getIt.get(); 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'; 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 d37f396..53da5a3 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 d5c8665..617c67d 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'; @@ -49,16 +50,19 @@ class ScreenConfigViewModel extends ChangeNotifier { } Future 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 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 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 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 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 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_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_sidelist.dart b/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart index c7fbc91..13b28ff 100644 --- a/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart +++ b/lib/views/main_queue_screen/components/priority_tickets_sidelist.dart @@ -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 ?? "", diff --git a/lib/views/main_queue_screen/main_queue_screen.dart b/lib/views/main_queue_screen/main_queue_screen.dart index 1915e2d..df71d51 100644 --- a/lib/views/main_queue_screen/main_queue_screen.dart +++ b/lib/views/main_queue_screen/main_queue_screen.dart @@ -192,7 +192,9 @@ class _MainQueueScreenState extends State { // context.read().createAutoTickets(numOfTicketsToCreate: 20); // context.read().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(),