From 07c704841d4bc7903c2c54b287913901d40fa840 Mon Sep 17 00:00:00 2001 From: Brandon Presley Date: Thu, 8 Feb 2024 20:58:08 +1300 Subject: [PATCH] Use foreground service for alarm completion Many of our errors in production are caused by the alarm module finishing. In devices after android version 7 we are "required" to use startForegroundService or else the following error supposedly occurs: Exception java.lang.IllegalStateException: at android.app.ContextImpl.startServiceCommon (ContextImpl.java:1725) at android.app.ContextImpl.startService (ContextImpl.java:1680) at android.content.ContextWrapper.startService (ContextWrapper.java:731) at android.content.ContextWrapper.startService (ContextWrapper.java:731) at com.massive.AlarmModule$getTimer$1.onFinish (AlarmModule.kt:144) at android.os.CountDownTimer$1.handleMessage (CountDownTimer.java:127) at android.os.Handler.dispatchMessage (Handler.java:106) at android.os.Looper.loop (Looper.java:236) at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run (MessageQueueThreadImpl.java:228) at java.lang.Thread.run (Thread.java:923) I say supposedly because on all of my testing devices (which are android 7+) this error doesn't occur. --- .../src/main/java/com/massive/AlarmModule.kt | 67 ++++++++++++------- .../src/main/java/com/massive/AlarmService.kt | 29 ++++---- 2 files changed, 56 insertions(+), 40 deletions(-) diff --git a/android/app/src/main/java/com/massive/AlarmModule.kt b/android/app/src/main/java/com/massive/AlarmModule.kt index a7642b5..8b252be 100644 --- a/android/app/src/main/java/com/massive/AlarmModule.kt +++ b/android/app/src/main/java/com/massive/AlarmModule.kt @@ -9,14 +9,14 @@ import android.content.IntentFilter import android.os.Build import android.os.CountDownTimer import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import kotlin.math.floor -class AlarmModule constructor(context: ReactApplicationContext?) : - ReactContextBaseJavaModule(context) { +@SuppressLint("UnspecifiedRegisterReceiverFlag") +class AlarmModule(context: ReactApplicationContext?) : + ReactContextBaseJavaModule(context), LifecycleEventListener { private var countdownTimer: CountDownTimer? = null var currentMs: Long = 0 @@ -29,7 +29,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) : private val stopReceiver = object : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context?, intent: Intent?) { Log.d("AlarmModule", "Received stop broadcast intent") stop() @@ -38,24 +37,29 @@ class AlarmModule constructor(context: ReactApplicationContext?) : private val addReceiver = object : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.O) override fun onReceive(context: Context?, intent: Intent?) { add() } } init { - reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) - reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST), + Context.RECEIVER_NOT_EXPORTED) + reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST), + Context.RECEIVER_NOT_EXPORTED) + } + else { + reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) + reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) + } } - override fun onCatalystInstanceDestroy() { + override fun onHostDestroy() { reactApplicationContext.unregisterReceiver(stopReceiver) reactApplicationContext.unregisterReceiver(addReceiver) - super.onCatalystInstanceDestroy() } - @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun add() { Log.d("AlarmModule", "Add 1 min to alarm.") @@ -77,7 +81,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) : return 0 } - @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun stop() { Log.d("AlarmModule", "Stop alarm.") @@ -98,7 +101,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) : .emit("tick", params) } - @RequiresApi(api = Build.VERSION_CODES.O) @ReactMethod fun timer(milliseconds: Int, description: String) { Log.d("AlarmModule", "Queue alarm for $milliseconds delay") @@ -113,13 +115,11 @@ class AlarmModule constructor(context: ReactApplicationContext?) : running = true } - @RequiresApi(Build.VERSION_CODES.M) private fun getTimer( endMs: Int, ): CountDownTimer { val builder = getBuilder() return object : CountDownTimer(endMs.toLong(), 1000) { - @RequiresApi(Build.VERSION_CODES.O) override fun onTick(current: Long) { currentMs = current val seconds = @@ -144,10 +144,15 @@ class AlarmModule constructor(context: ReactApplicationContext?) : .emit("tick", params) } - @RequiresApi(Build.VERSION_CODES.O) override fun onFinish() { val context = reactApplicationContext - context.startService(Intent(context, AlarmService::class.java)) + val intent = Intent(context, AlarmService::class.java) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } + else { + context.startService(intent) + } context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit( "tick", @@ -161,7 +166,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) : } @SuppressLint("UnspecifiedImmutableFlag") - @RequiresApi(Build.VERSION_CODES.M) private fun getBuilder(): NotificationCompat.Builder { val context = reactApplicationContext val contentIntent = Intent(context, MainActivity::class.java) @@ -183,19 +187,22 @@ class AlarmModule constructor(context: ReactApplicationContext?) : .setDeleteIntent(pendingStop) } - @RequiresApi(Build.VERSION_CODES.O) private fun getManager(): NotificationManager { val notificationManager = - reactApplicationContext.getSystemService(NotificationManager::class.java) - val timersChannel = + reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val timersChannel = NotificationChannel( - CHANNEL_ID_PENDING, - CHANNEL_ID_PENDING, - NotificationManager.IMPORTANCE_LOW + CHANNEL_ID_PENDING, + CHANNEL_ID_PENDING, + NotificationManager.IMPORTANCE_LOW ) - timersChannel.setSound(null, null) - timersChannel.description = "Progress on rest timers." - notificationManager.createNotificationChannel(timersChannel) + timersChannel.setSound(null, null) + timersChannel.description = "Progress on rest timers." + notificationManager.createNotificationChannel(timersChannel) + } + return notificationManager } @@ -205,4 +212,12 @@ class AlarmModule constructor(context: ReactApplicationContext?) : const val CHANNEL_ID_PENDING = "Timer" const val NOTIFICATION_ID_PENDING = 1 } + + override fun onHostResume() { + TODO("Not yet implemented") + } + + override fun onHostPause() { + TODO("Not yet implemented") + } } diff --git a/android/app/src/main/java/com/massive/AlarmService.kt b/android/app/src/main/java/com/massive/AlarmService.kt index 442a3b4..f6dd636 100644 --- a/android/app/src/main/java/com/massive/AlarmService.kt +++ b/android/app/src/main/java/com/massive/AlarmService.kt @@ -14,7 +14,6 @@ import androidx.core.app.NotificationCompat class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long) -@RequiresApi(Build.VERSION_CODES.O) class AlarmService : Service(), OnPreparedListener { private var mediaPlayer: MediaPlayer? = null private var vibrator: Vibrator? = null @@ -58,7 +57,7 @@ class AlarmService : Service(), OnPreparedListener { } private fun playSound(settings: Settings) { - if (settings.noSound) return; + if (settings.noSound) return if (settings.sound == null) { mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon) mediaPlayer?.start() @@ -80,18 +79,20 @@ class AlarmService : Service(), OnPreparedListener { } private fun doNotify(): Notification { - val alarmsChannel = NotificationChannel( - CHANNEL_ID_DONE, - CHANNEL_ID_DONE, - NotificationManager.IMPORTANCE_HIGH - ) - alarmsChannel.description = "Alarms for rest timers." - alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC - alarmsChannel.setSound(null, null) - val manager = applicationContext.getSystemService( - NotificationManager::class.java - ) - manager.createNotificationChannel(alarmsChannel) + val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val alarmsChannel = NotificationChannel( + CHANNEL_ID_DONE, + CHANNEL_ID_DONE, + NotificationManager.IMPORTANCE_HIGH + ) + alarmsChannel.description = "Alarms for rest timers." + alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC + alarmsChannel.setSound(null, null) + manager.createNotificationChannel(alarmsChannel) + } + val builder = getBuilder() val context = applicationContext val finishIntent = Intent(context, StopAlarm::class.java)