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.
This commit is contained in:
Brandon Presley 2024-02-08 20:58:08 +13:00
parent 1f6100607d
commit 07c704841d
2 changed files with 56 additions and 40 deletions

View File

@ -9,14 +9,14 @@ import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.os.CountDownTimer import android.os.CountDownTimer
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import com.facebook.react.bridge.* import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.floor import kotlin.math.floor
class AlarmModule constructor(context: ReactApplicationContext?) : @SuppressLint("UnspecifiedRegisterReceiverFlag")
ReactContextBaseJavaModule(context) { class AlarmModule(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context), LifecycleEventListener {
private var countdownTimer: CountDownTimer? = null private var countdownTimer: CountDownTimer? = null
var currentMs: Long = 0 var currentMs: Long = 0
@ -29,7 +29,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
private val stopReceiver = private val stopReceiver =
object : BroadcastReceiver() { object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
Log.d("AlarmModule", "Received stop broadcast intent") Log.d("AlarmModule", "Received stop broadcast intent")
stop() stop()
@ -38,24 +37,29 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
private val addReceiver = private val addReceiver =
object : BroadcastReceiver() { object : BroadcastReceiver() {
@RequiresApi(Build.VERSION_CODES.O)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
add() add()
} }
} }
init { init {
reactApplicationContext.registerReceiver(stopReceiver, IntentFilter(STOP_BROADCAST)) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
reactApplicationContext.registerReceiver(addReceiver, IntentFilter(ADD_BROADCAST)) 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(stopReceiver)
reactApplicationContext.unregisterReceiver(addReceiver) reactApplicationContext.unregisterReceiver(addReceiver)
super.onCatalystInstanceDestroy()
} }
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun add() { fun add() {
Log.d("AlarmModule", "Add 1 min to alarm.") Log.d("AlarmModule", "Add 1 min to alarm.")
@ -77,7 +81,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
return 0 return 0
} }
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun stop() { fun stop() {
Log.d("AlarmModule", "Stop alarm.") Log.d("AlarmModule", "Stop alarm.")
@ -98,7 +101,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
.emit("tick", params) .emit("tick", params)
} }
@RequiresApi(api = Build.VERSION_CODES.O)
@ReactMethod @ReactMethod
fun timer(milliseconds: Int, description: String) { fun timer(milliseconds: Int, description: String) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
@ -113,13 +115,11 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
running = true running = true
} }
@RequiresApi(Build.VERSION_CODES.M)
private fun getTimer( private fun getTimer(
endMs: Int, endMs: Int,
): CountDownTimer { ): CountDownTimer {
val builder = getBuilder() val builder = getBuilder()
return object : CountDownTimer(endMs.toLong(), 1000) { return object : CountDownTimer(endMs.toLong(), 1000) {
@RequiresApi(Build.VERSION_CODES.O)
override fun onTick(current: Long) { override fun onTick(current: Long) {
currentMs = current currentMs = current
val seconds = val seconds =
@ -144,10 +144,15 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
.emit("tick", params) .emit("tick", params)
} }
@RequiresApi(Build.VERSION_CODES.O)
override fun onFinish() { override fun onFinish() {
val context = reactApplicationContext 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) context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit( .emit(
"tick", "tick",
@ -161,7 +166,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
} }
@SuppressLint("UnspecifiedImmutableFlag") @SuppressLint("UnspecifiedImmutableFlag")
@RequiresApi(Build.VERSION_CODES.M)
private fun getBuilder(): NotificationCompat.Builder { private fun getBuilder(): NotificationCompat.Builder {
val context = reactApplicationContext val context = reactApplicationContext
val contentIntent = Intent(context, MainActivity::class.java) val contentIntent = Intent(context, MainActivity::class.java)
@ -183,19 +187,22 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
.setDeleteIntent(pendingStop) .setDeleteIntent(pendingStop)
} }
@RequiresApi(Build.VERSION_CODES.O)
private fun getManager(): NotificationManager { private fun getManager(): NotificationManager {
val notificationManager = val notificationManager =
reactApplicationContext.getSystemService(NotificationManager::class.java) reactApplicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val timersChannel =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val timersChannel =
NotificationChannel( NotificationChannel(
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING,
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING,
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
timersChannel.setSound(null, null) timersChannel.setSound(null, null)
timersChannel.description = "Progress on rest timers." timersChannel.description = "Progress on rest timers."
notificationManager.createNotificationChannel(timersChannel) notificationManager.createNotificationChannel(timersChannel)
}
return notificationManager return notificationManager
} }
@ -205,4 +212,12 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
const val CHANNEL_ID_PENDING = "Timer" const val CHANNEL_ID_PENDING = "Timer"
const val NOTIFICATION_ID_PENDING = 1 const val NOTIFICATION_ID_PENDING = 1
} }
override fun onHostResume() {
TODO("Not yet implemented")
}
override fun onHostPause() {
TODO("Not yet implemented")
}
} }

View File

@ -14,7 +14,6 @@ import androidx.core.app.NotificationCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long) class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean, val duration: Long)
@RequiresApi(Build.VERSION_CODES.O)
class AlarmService : Service(), OnPreparedListener { class AlarmService : Service(), OnPreparedListener {
private var mediaPlayer: MediaPlayer? = null private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null private var vibrator: Vibrator? = null
@ -58,7 +57,7 @@ class AlarmService : Service(), OnPreparedListener {
} }
private fun playSound(settings: Settings) { private fun playSound(settings: Settings) {
if (settings.noSound) return; if (settings.noSound) return
if (settings.sound == null) { if (settings.sound == null) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon) mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start() mediaPlayer?.start()
@ -80,18 +79,20 @@ class AlarmService : Service(), OnPreparedListener {
} }
private fun doNotify(): Notification { private fun doNotify(): Notification {
val alarmsChannel = NotificationChannel( val manager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
CHANNEL_ID_DONE,
CHANNEL_ID_DONE, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager.IMPORTANCE_HIGH val alarmsChannel = NotificationChannel(
) CHANNEL_ID_DONE,
alarmsChannel.description = "Alarms for rest timers." CHANNEL_ID_DONE,
alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC NotificationManager.IMPORTANCE_HIGH
alarmsChannel.setSound(null, null) )
val manager = applicationContext.getSystemService( alarmsChannel.description = "Alarms for rest timers."
NotificationManager::class.java alarmsChannel.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
) alarmsChannel.setSound(null, null)
manager.createNotificationChannel(alarmsChannel) manager.createNotificationChannel(alarmsChannel)
}
val builder = getBuilder() val builder = getBuilder()
val context = applicationContext val context = applicationContext
val finishIntent = Intent(context, StopAlarm::class.java) val finishIntent = Intent(context, StopAlarm::class.java)