Start alarms as a foreground service

Related to #142.
Can't be sure this fixed anything because I can't replicate the error
on my emulators running android 13. I need to install android 13
on a real device and try replicate + see if this fixes it.
This commit is contained in:
Brandon Presley 2023-01-03 17:04:51 +13:00
parent 05237fc293
commit cffc458338
3 changed files with 97 additions and 67 deletions

View File

@ -63,7 +63,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
countdownTimer?.start() countdownTimer?.start()
running = true running = true
val manager = getManager() val manager = getManager()
manager.cancel(NOTIFICATION_ID_DONE) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
val intent = Intent(reactApplicationContext, AlarmService::class.java) val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(intent) reactApplicationContext.stopService(intent)
} }
@ -77,7 +77,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
val intent = Intent(reactApplicationContext, AlarmService::class.java) val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext?.stopService(intent) reactApplicationContext?.stopService(intent)
val manager = getManager() val manager = getManager()
manager.cancel(NOTIFICATION_ID_DONE) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(NOTIFICATION_ID_PENDING) manager.cancel(NOTIFICATION_ID_PENDING)
val params = Arguments.createMap().apply { val params = Arguments.createMap().apply {
putString("minutes", "00") putString("minutes", "00")
@ -93,7 +93,7 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
fun timer(milliseconds: Int) { fun timer(milliseconds: Int) {
Log.d("AlarmModule", "Queue alarm for $milliseconds delay") Log.d("AlarmModule", "Queue alarm for $milliseconds delay")
val manager = getManager() val manager = getManager()
manager.cancel(NOTIFICATION_ID_DONE) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
val intent = Intent(reactApplicationContext, AlarmService::class.java) val intent = Intent(reactApplicationContext, AlarmService::class.java)
reactApplicationContext.stopService(intent) reactApplicationContext.stopService(intent)
countdownTimer?.cancel() countdownTimer?.cancel()
@ -133,25 +133,8 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
override fun onFinish() { override fun onFinish() {
val context = reactApplicationContext val context = reactApplicationContext
val finishIntent = Intent(context, StopAlarm::class.java) context.startForegroundService(Intent(context, AlarmService::class.java))
val finishPending = PendingIntent.getActivity( context
context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val fullIntent = Intent(context, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
context, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.setContentText("Timer finished.").setProgress(0, 0, false)
.setAutoCancel(true).setOngoing(true).setFullScreenIntent(fullPending, true)
.setContentIntent(finishPending).setChannelId(CHANNEL_ID_DONE)
.setCategory(NotificationCompat.CATEGORY_ALARM).priority =
NotificationCompat.PRIORITY_HIGH
val manager = getManager()
manager.notify(NOTIFICATION_ID_DONE, builder.build())
manager.cancel(NOTIFICATION_ID_PENDING)
val alarmIntent = Intent(context, AlarmService::class.java)
context.startService(alarmIntent)
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit("finish", Arguments.createMap().apply { .emit("finish", Arguments.createMap().apply {
putString("minutes", "00") putString("minutes", "00")
@ -169,12 +152,12 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
val pendingContent = val pendingContent =
PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
val addBroadcast = Intent(ADD_BROADCAST).apply { val addBroadcast = Intent(ADD_BROADCAST).apply {
setPackage(reactApplicationContext.packageName) setPackage(context.packageName)
} }
val pendingAdd = val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE) PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(STOP_BROADCAST) val stopBroadcast = Intent(STOP_BROADCAST)
stopBroadcast.setPackage(reactApplicationContext.packageName) stopBroadcast.setPackage(context.packageName)
val pendingStop = val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE) PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, CHANNEL_ID_PENDING) return NotificationCompat.Builder(context, CHANNEL_ID_PENDING)
@ -187,16 +170,9 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
private fun getManager(): NotificationManager { private fun getManager(): NotificationManager {
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 notificationManager = reactApplicationContext.getSystemService( val notificationManager = reactApplicationContext.getSystemService(
NotificationManager::class.java NotificationManager::class.java
) )
notificationManager.createNotificationChannel(alarmsChannel)
val timersChannel = NotificationChannel( val timersChannel = NotificationChannel(
CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW CHANNEL_ID_PENDING, CHANNEL_ID_PENDING, NotificationManager.IMPORTANCE_LOW
) )
@ -210,8 +186,6 @@ class AlarmModule constructor(context: ReactApplicationContext?) :
const val STOP_BROADCAST = "stop-timer-event" const val STOP_BROADCAST = "stop-timer-event"
const val ADD_BROADCAST = "add-timer-event" const val ADD_BROADCAST = "add-timer-event"
const val CHANNEL_ID_PENDING = "Timer" const val CHANNEL_ID_PENDING = "Timer"
const val CHANNEL_ID_DONE = "Alarm"
const val NOTIFICATION_ID_PENDING = 1 const val NOTIFICATION_ID_PENDING = 1
const val NOTIFICATION_ID_DONE = 2
} }
} }

View File

@ -1,7 +1,7 @@
package com.massive package com.massive
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Service import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.AudioAttributes import android.media.AudioAttributes
@ -9,42 +9,57 @@ import android.media.MediaPlayer
import android.media.MediaPlayer.OnPreparedListener import android.media.MediaPlayer.OnPreparedListener
import android.net.Uri import android.net.Uri
import android.os.* import android.os.*
import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
class Settings(val sound: String?, val noSound: Boolean, val vibrate: Boolean)
@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
@SuppressLint("Recycle") private fun getBuilder(): NotificationCompat.Builder {
@RequiresApi(api = Build.VERSION_CODES.O) val context = applicationContext
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val contentIntent = Intent(context, MainActivity::class.java)
if (intent.action == "stop") { val pendingContent =
onDestroy() PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_IMMUTABLE)
return START_STICKY val addBroadcast = Intent(AlarmModule.ADD_BROADCAST).apply {
setPackage(context.packageName)
} }
val pendingAdd =
PendingIntent.getBroadcast(context, 0, addBroadcast, PendingIntent.FLAG_MUTABLE)
val stopBroadcast = Intent(AlarmModule.STOP_BROADCAST)
stopBroadcast.setPackage(context.packageName)
val pendingStop =
PendingIntent.getBroadcast(context, 0, stopBroadcast, PendingIntent.FLAG_IMMUTABLE)
return NotificationCompat.Builder(context, AlarmModule.CHANNEL_ID_PENDING)
.setSmallIcon(R.drawable.ic_baseline_hourglass_bottom_24).setContentTitle("Resting")
.setContentIntent(pendingContent)
.addAction(R.drawable.ic_baseline_stop_24, "Stop", pendingStop)
.addAction(R.drawable.ic_baseline_stop_24, "Add 1 min", pendingAdd)
.setDeleteIntent(pendingStop)
}
@SuppressLint("Range")
private fun getSettings(): Settings {
val db = DatabaseHelper(applicationContext).readableDatabase val db = DatabaseHelper(applicationContext).readableDatabase
val cursor = db.rawQuery("SELECT sound, noSound, vibrate FROM settings", null)
cursor.moveToFirst()
val sound = cursor.getString(cursor.getColumnIndex("sound"))
val noSound = cursor.getInt(cursor.getColumnIndex("noSound")) == 1
val vibrate = cursor.getInt(cursor.getColumnIndex("vibrate")) == 1
cursor.close()
return Settings(sound, noSound, vibrate)
}
val sound = db.rawQuery("SELECT sound FROM settings", null) private fun playSound(settings: Settings) {
.let { if (settings.sound == null && !settings.noSound) {
it.moveToFirst()
it.getString(0)
}
Log.d("AlarmService", "sound=$sound")
val noSound = db.rawQuery("SELECT noSound FROM settings", null)
.let {
it.moveToFirst()
it.getInt(0) == 1
}
Log.d("AlarmService", "noSound=$noSound")
if (sound == null && !noSound) {
mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon) mediaPlayer = MediaPlayer.create(applicationContext, R.raw.argon)
mediaPlayer?.start() mediaPlayer?.start()
mediaPlayer?.setOnCompletionListener { vibrator?.cancel() } mediaPlayer?.setOnCompletionListener { vibrator?.cancel() }
} else if (sound != null && !noSound) { } else if (settings.sound != null && !settings.noSound) {
mediaPlayer = MediaPlayer().apply { mediaPlayer = MediaPlayer().apply {
setAudioAttributes( setAudioAttributes(
AudioAttributes.Builder() AudioAttributes.Builder()
@ -52,20 +67,56 @@ class AlarmService : Service(), OnPreparedListener {
.setUsage(AudioAttributes.USAGE_MEDIA) .setUsage(AudioAttributes.USAGE_MEDIA)
.build() .build()
) )
setDataSource(applicationContext, Uri.parse(sound)) setDataSource(applicationContext, Uri.parse(settings.sound))
prepare() prepare()
start() start()
setOnCompletionListener { vibrator?.cancel() } setOnCompletionListener { vibrator?.cancel() }
} }
} }
}
val vibrate = db.rawQuery("SELECT vibrate FROM settings", null) private fun doNotify(): Notification {
.let { val alarmsChannel = NotificationChannel(
it.moveToFirst() CHANNEL_ID_DONE,
it.getInt(0) == 1 CHANNEL_ID_DONE,
} NotificationManager.IMPORTANCE_HIGH
if (!vibrate) return START_STICKY )
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 builder = getBuilder()
val context = applicationContext
val finishIntent = Intent(context, StopAlarm::class.java)
val finishPending = PendingIntent.getActivity(
context, 0, finishIntent, PendingIntent.FLAG_IMMUTABLE
)
val fullIntent = Intent(context, TimerDone::class.java)
val fullPending = PendingIntent.getActivity(
context, 0, fullIntent, PendingIntent.FLAG_IMMUTABLE
)
builder.setContentText("Timer finished.").setProgress(0, 0, false)
.setAutoCancel(true).setOngoing(true).setFullScreenIntent(fullPending, true)
.setContentIntent(finishPending).setChannelId(CHANNEL_ID_DONE)
.setCategory(NotificationCompat.CATEGORY_ALARM).priority =
NotificationCompat.PRIORITY_HIGH
val notification = builder.build()
manager.notify(NOTIFICATION_ID_DONE, notification)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
return notification
}
@SuppressLint("Recycle")
@RequiresApi(api = Build.VERSION_CODES.O)
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val notification = doNotify()
startForeground(NOTIFICATION_ID_DONE, notification)
val settings = getSettings()
playSound(settings)
if (!settings.vibrate) return START_STICKY
val pattern = longArrayOf(0, 300, 1300, 300, 1300, 300) val pattern = longArrayOf(0, 300, 1300, 300, 1300, 300)
vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = val vibratorManager =
@ -97,4 +148,9 @@ class AlarmService : Service(), OnPreparedListener {
mediaPlayer?.release() mediaPlayer?.release()
vibrator?.cancel() vibrator?.cancel()
} }
companion object {
const val CHANNEL_ID_DONE = "Alarm"
const val NOTIFICATION_ID_DONE = 2
}
} }

View File

@ -23,7 +23,7 @@ class TimerDone : AppCompatActivity() {
Log.d("TimerDone", "Stopping...") Log.d("TimerDone", "Stopping...")
applicationContext.stopService(Intent(applicationContext, AlarmService::class.java)) applicationContext.stopService(Intent(applicationContext, AlarmService::class.java))
val manager = getManager() val manager = getManager()
manager.cancel(AlarmModule.NOTIFICATION_ID_DONE) manager.cancel(AlarmService.NOTIFICATION_ID_DONE)
manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING) manager.cancel(AlarmModule.NOTIFICATION_ID_PENDING)
val intent = Intent(applicationContext, MainActivity::class.java) val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
@ -33,8 +33,8 @@ class TimerDone : AppCompatActivity() {
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun getManager(): NotificationManager { fun getManager(): NotificationManager {
val alarmsChannel = NotificationChannel( val alarmsChannel = NotificationChannel(
AlarmModule.CHANNEL_ID_DONE, AlarmService.CHANNEL_ID_DONE,
AlarmModule.CHANNEL_ID_DONE, AlarmService.CHANNEL_ID_DONE,
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
description = "Alarms for rest timers." description = "Alarms for rest timers."