From 58b8488a272ddb016475e9843a3f69af6ca5fb7e 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 | 171 ++++++++++++++++++ .../src/main/java/com/massive/TimerService.kt | 82 +++++---- 3 files changed, 223 insertions(+), 31 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.TIRAMISU) { + context.registerReceiver( + receiver, + IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + context.registerReceiver( + receiver, + IntentFilter(AlarmManager.ACTION_SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED) + ) + } + + context.startActivity(intent) + false + } catch (e: ActivityNotFoundException) { + Toast.makeText( + context, + "Request for SCHEDULE_EXACT_ALARM rejected on your device", + Toast.LENGTH_LONG + ).show() + false + } + } + + private fun incorrectPermissions(context: Context, alarmManager: AlarmManager): Boolean { + return Build.VERSION.SDK_INT >= 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..bc370db 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,10 @@ class TimerService : Service() { object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { Log.d("TimerService", "Received stop broadcast intent") + timer.stop(applicationContext) + timer.expire() + + stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } } @@ -45,9 +48,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,49 +71,62 @@ 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() - } + val startTime = SystemClock.elapsedRealtime() + if (timer.isExpired()) return + updateNotification(timer.getRemainingSeconds()) + + val delay = timer.getRemainingMillis() % 1000 + timerHandler.postDelayed(this, if (SystemClock.elapsedRealtime() - startTime + delay > 980) 20 else delay) } } - timerHandler.postDelayed(timerRunnable!!, 1000) + timerHandler.postDelayed(timerRunnable!!, 20) + } + + 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 } override fun onDestroy() { super.onDestroy() - timerHandler.removeCallbacks(timerRunnable!!) + timerRunnable?.let { timerHandler.removeCallbacks(it) } applicationContext.unregisterReceiver(stopReceiver) applicationContext.unregisterReceiver(addReceiver) mediaPlayer?.stop() @@ -217,7 +232,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 +276,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 +351,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 }