From e3ca490ef247372280d1a0d12af896c59a35dee7 Mon Sep 17 00:00:00 2001 From: Joseph Date: Fri, 8 Mar 2024 20:27:01 +0000 Subject: [PATCH] Use AlarmManager to manage the ending of timers --- android/app/src/main/AndroidManifest.xml | 1 + .../app/src/main/java/com/massive/Timer.kt | 147 ++++++++++++++++++ .../src/main/java/com/massive/TimerService.kt | 73 +++++---- 3 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 android/app/src/main/java/com/massive/Timer.kt diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c969055..a7ba2ce 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + = Build.VERSION_CODES.S + && !alarmManager.canScheduleExactAlarms() + && !requestPermission(context) + } + + private fun getAlarmManager(context: Context): AlarmManager { + return context.getSystemService(Context.ALARM_SERVICE) as AlarmManager + } + + private fun unregisterPendingIntent(context: Context) { + val intent = Intent(context, TimerService::class.java) + .setAction(TimerService.TIMER_EXPIRED) + val pendingIntent = PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_NO_CREATE or PendingIntent.FLAG_IMMUTABLE + ) + val alarmManager = getAlarmManager(context) + if (incorrectPermissions(context, alarmManager)) return + + alarmManager.cancel(pendingIntent) + pendingIntent.cancel() + } + + private fun registerPendingIntent(context: Context) { + val intent = Intent(context, TimerService::class.java) + .setAction(TimerService.TIMER_EXPIRED) + val pendingIntent = PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + val alarmManager = getAlarmManager(context) + if (incorrectPermissions(context, alarmManager)) return + + alarmManager.setExactAndAllowWhileIdle( + ELAPSED_REALTIME_WAKEUP, + endTime, + pendingIntent + ) + } + + private var endTime: Long = 0 + private var totalTimerDuration: Long = msTimerDuration + private var state: State = State.Paused + + companion object { + fun emptyTimer(): Timer { + return Timer(0) + } + + const val ONE_MINUTE_MILLI: Long = 60000 + } +} diff --git a/android/app/src/main/java/com/massive/TimerService.kt b/android/app/src/main/java/com/massive/TimerService.kt index efb4997..3e722d5 100644 --- a/android/app/src/main/java/com/massive/TimerService.kt +++ b/android/app/src/main/java/com/massive/TimerService.kt @@ -28,8 +28,7 @@ class TimerService : Service() { private lateinit var timerHandler: Handler private var timerRunnable: Runnable? = null - private var secondsLeft: Int = 0 - private var secondsTotal: Int = 0 + private var timer: Timer = Timer.emptyTimer() private var mediaPlayer: MediaPlayer? = null private var vibrator: Vibrator? = null private var currentDescription = "" @@ -38,6 +37,8 @@ class TimerService : Service() { object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { Log.d("TimerService", "Received stop broadcast intent") + timer.stop(applicationContext) + timer.expire() stopSelf() } } @@ -45,9 +46,8 @@ class TimerService : Service() { private val addReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - secondsLeft += 60; - secondsTotal += 60; - updateNotification(secondsLeft) + timer.increaseDuration(applicationContext, Timer.ONE_MINUTE_MILLI) + updateNotification(timer.getRemainingSeconds()) mediaPlayer?.stop() vibrator?.cancel() } @@ -69,43 +69,53 @@ class TimerService : Service() { } else { applicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) applicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) - } + } } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + private fun onTimerStart(intent: Intent?) { timerRunnable?.let { timerHandler.removeCallbacks(it) } - secondsLeft = (intent?.getIntExtra("milliseconds", 0) ?: 0) / 1000 currentDescription = intent?.getStringExtra("description").toString() - secondsTotal = secondsLeft - val startTime = System.currentTimeMillis() + + timer.stop(applicationContext) + timer = Timer((intent?.getIntExtra("milliseconds", 0) ?: 0).toLong()) + timer.start(applicationContext) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - startForeground(ONGOING_ID, getProgress(secondsLeft).build(), FOREGROUND_SERVICE_TYPE_SPECIAL_USE) - } else - { - startForeground(ONGOING_ID, getProgress(secondsLeft).build()) + startForeground( + ONGOING_ID, + getProgress(timer.getRemainingSeconds()).build(), + FOREGROUND_SERVICE_TYPE_SPECIAL_USE + ) + } else { + startForeground(ONGOING_ID, getProgress(timer.getRemainingSeconds()).build()) } battery() - Log.d("TimerService", "onStartCommand seconds=$secondsTotal") + Log.d("TimerService", "onTimerStart seconds=${timer.getDurationSeconds()}") timerRunnable = object : Runnable { override fun run() { - val millisElapsed = System.currentTimeMillis() - startTime - val secondsElapsed = (millisElapsed / 1000).toInt() - if (secondsElapsed < secondsTotal) { - secondsLeft = secondsTotal - secondsElapsed - updateNotification(secondsLeft) - timerHandler.postDelayed(this, 1000 - millisElapsed % 1000) - } else { - val settings = getSettings() - vibrate(settings) - playSound(settings) - notifyFinished() - } + if (timer.isExpired()) return + updateNotification(timer.getRemainingSeconds()) + timerHandler.postDelayed(this, timer.getRemainingMillis() % 1000) } } timerHandler.postDelayed(timerRunnable!!, 1000) + } + + private fun onTimerExpired() { + Log.d("TimerService", "onTimerExpired duration=${timer.getDurationSeconds()}") + timer.expire() + + val settings = getSettings() + vibrate(settings) + playSound(settings) + notifyFinished() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null && intent.action == TIMER_EXPIRED) onTimerExpired() + else onTimerStart(intent) return START_STICKY } @@ -217,7 +227,7 @@ class TimerService : Service() { .setContentTitle(currentDescription) .setContentText(notificationText) .setSmallIcon(R.drawable.ic_baseline_timer_24) - .setProgress(secondsTotal, timeLeftInSeconds, false) + .setProgress(timer.getDurationSeconds(), timeLeftInSeconds, false) .setContentIntent(contentPending) .setCategory(NotificationCompat.CATEGORY_PROGRESS) .setAutoCancel(false) @@ -261,7 +271,11 @@ class TimerService : Service() { val notificationManager = NotificationManagerCompat.from(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = - NotificationChannel(channelId, "Timer Finished Channel", NotificationManager.IMPORTANCE_HIGH) + NotificationChannel( + channelId, + "Timer Finished Channel", + NotificationManager.IMPORTANCE_HIGH + ) channel.setSound(null, null) channel.setBypassDnd(true) channel.enableVibration(false) @@ -332,6 +346,7 @@ class TimerService : Service() { companion object { const val STOP_BROADCAST = "stop-timer-event" const val ADD_BROADCAST = "add-timer-event" + const val TIMER_EXPIRED = "timer-expired-event" const val ONGOING_ID = 1 const val FINISHED_ID = 1 }