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.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")
}
}

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)
@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)