Use a foreground service for sounding alarms

- Simplified the overengineered on/off screen notification logic. Alarms now use a notification with a full screen intent for reminder activity regardless of the device state.
- Added a foreground service, the app will no longer rely on repeating notification sounds or the reminder activity for sounding alarms.
- Simplified the reminder activity by delegating some responsibilities to the foreground service
This commit is contained in:
Naveen Singh
2025-03-21 18:33:55 +05:30
parent fe498c9b4a
commit 7a7e53ed09
8 changed files with 277 additions and 308 deletions

View File

@@ -141,6 +141,14 @@
</intent-filter>
</activity>
<service
android:name=".services.AlarmService"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="Used to notify the user that alarms are running" />
</service>
<service android:name=".services.SnoozeService" />
<service

View File

@@ -3,18 +3,13 @@ package org.fossify.clock.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import android.provider.AlarmClock
import android.view.MotionEvent
import android.view.WindowManager
import android.view.animation.AnimationUtils
import androidx.core.net.toUri
import org.fossify.clock.R
import org.fossify.clock.databinding.ActivityReminderBinding
import org.fossify.clock.extensions.cancelAlarmClock
@@ -24,8 +19,8 @@ import org.fossify.clock.extensions.disableExpiredAlarm
import org.fossify.clock.extensions.getFormattedTime
import org.fossify.clock.extensions.scheduleNextAlarm
import org.fossify.clock.extensions.setupAlarmClock
import org.fossify.clock.extensions.stopAlarmService
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.clock.helpers.ALARM_NOTIF_ID
import org.fossify.clock.helpers.getPassedSeconds
import org.fossify.clock.models.Alarm
import org.fossify.commons.extensions.applyColorFilter
@@ -34,41 +29,25 @@ import org.fossify.commons.extensions.getColoredDrawableWithColor
import org.fossify.commons.extensions.getProperBackgroundColor
import org.fossify.commons.extensions.getProperPrimaryColor
import org.fossify.commons.extensions.getProperTextColor
import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.extensions.onGlobalLayout
import org.fossify.commons.extensions.performHapticFeedback
import org.fossify.commons.extensions.showPickSecondsDialog
import org.fossify.commons.extensions.updateTextColors
import org.fossify.commons.extensions.viewBinding
import org.fossify.commons.helpers.MINUTE_SECONDS
import org.fossify.commons.helpers.SILENT
import org.fossify.commons.helpers.isOreoMr1Plus
import org.fossify.commons.helpers.isOreoPlus
import java.util.Calendar
import kotlin.math.max
import kotlin.math.min
class ReminderActivity : SimpleActivity() {
companion object {
private const val MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS = 1
private const val INCREASE_VOLUME_DELAY = 300L
}
private val increaseVolumeHandler = Handler(Looper.getMainLooper())
private val maxReminderDurationHandler = Handler(Looper.getMainLooper())
private val swipeGuideFadeHandler = Handler()
private val vibrationHandler = Handler(Looper.getMainLooper())
private var isAlarmReminder = false
private var didVibrate = false
private var wasAlarmSnoozed = false
private val swipeGuideFadeHandler = Handler(Looper.getMainLooper())
private var alarm: Alarm? = null
private var audioManager: AudioManager? = null
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private var initialAlarmVolume: Int? = null
private var didVibrate = false
private var dragDownX = 0f
private var wasAlarmSnoozed = false
private val binding: ActivityReminderBinding by viewBinding(ActivityReminderBinding::inflate)
private var finished = false
override fun onCreate(savedInstanceState: Bundle?) {
isMaterialActivity = true
@@ -79,9 +58,13 @@ class ReminderActivity : SimpleActivity() {
updateStatusbarColor(getProperBackgroundColor())
val id = intent.getIntExtra(ALARM_ID, -1)
isAlarmReminder = id != -1
if (id != -1) {
alarm = dbHelper.getAlarmWithId(id) ?: return
val isAlarmReminder = id != -1
if (isAlarmReminder) {
alarm = dbHelper.getAlarmWithId(id)
if (alarm == null) {
finish()
return
}
}
val label = if (isAlarmReminder) {
@@ -105,22 +88,10 @@ class ReminderActivity : SimpleActivity() {
getString(R.string.time_expired)
}
val maxDuration = if (isAlarmReminder) {
config.alarmMaxReminderSecs
} else {
config.timerMaxReminderSecs
}
maxReminderDurationHandler.postDelayed({
finishActivity()
cancelNotification()
}, maxDuration * 1000L)
setupButtons()
setupEffects()
setupButtons(isAlarmReminder)
}
private fun setupButtons() {
private fun setupButtons(isAlarmReminder: Boolean) {
if (isAlarmReminder) {
setupAlarmButtons()
} else {
@@ -132,10 +103,7 @@ class ReminderActivity : SimpleActivity() {
private fun setupAlarmButtons() {
binding.reminderStop.beGone()
binding.reminderDraggableBackground.startAnimation(
AnimationUtils.loadAnimation(
this,
R.anim.pulsing_animation
)
AnimationUtils.loadAnimation(this, R.anim.pulsing_animation)
)
binding.reminderDraggableBackground.applyColorFilter(getProperPrimaryColor())
@@ -154,7 +122,7 @@ class ReminderActivity : SimpleActivity() {
initialDraggableX = binding.reminderDraggable.left.toFloat()
}
binding.reminderDraggable.setOnTouchListener { v, event ->
binding.reminderDraggable.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
dragDownX = event.x
@@ -188,16 +156,12 @@ class ReminderActivity : SimpleActivity() {
didVibrate = true
finishActivity()
}
cancelNotification()
} else if (binding.reminderDraggable.x <= minDragX + 50f) {
if (!didVibrate) {
binding.reminderDraggable.performHapticFeedback()
didVibrate = true
snoozeAlarm()
}
cancelNotification()
}
}
}
@@ -224,61 +188,6 @@ class ReminderActivity : SimpleActivity() {
}
}
private fun setupEffects() {
audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
initialAlarmVolume = audioManager?.getStreamVolume(AudioManager.STREAM_ALARM) ?: 7
val doVibrate = alarm?.vibrate ?: config.timerVibrate
if (doVibrate && isOreoPlus()) {
val pattern = LongArray(2) { 500 }
vibrationHandler.postDelayed({
vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
}, 500)
}
val soundUri = if (alarm != null) {
alarm!!.soundUri
} else {
config.timerSoundUri
}
if (soundUri != SILENT) {
try {
mediaPlayer = MediaPlayer().apply {
setAudioStreamType(AudioManager.STREAM_ALARM)
setDataSource(this@ReminderActivity, soundUri.toUri())
isLooping = true
prepare()
start()
}
if (config.increaseVolumeGradually) {
scheduleVolumeIncrease(
lastVolume = MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS.toFloat(),
maxVolume = initialAlarmVolume!!.toFloat(),
delay = 0
)
}
} catch (e: Exception) {
}
}
}
private fun scheduleVolumeIncrease(lastVolume: Float, maxVolume: Float, delay: Long) {
increaseVolumeHandler.postDelayed({
val newLastVolume = (lastVolume + 0.1f).coerceAtMost(maxVolume)
audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, newLastVolume.toInt(), 0)
scheduleVolumeIncrease(newLastVolume, maxVolume, INCREASE_VOLUME_DELAY)
}, delay)
}
private fun resetVolumeToInitialValue() {
initialAlarmVolume?.apply {
audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, this, 0)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
setupAlarmButtons()
@@ -300,32 +209,11 @@ class ReminderActivity : SimpleActivity() {
override fun onDestroy() {
super.onDestroy()
increaseVolumeHandler.removeCallbacksAndMessages(null)
maxReminderDurationHandler.removeCallbacksAndMessages(null)
swipeGuideFadeHandler.removeCallbacksAndMessages(null)
vibrationHandler.removeCallbacksAndMessages(null)
if (!finished) {
finishActivity()
cancelNotification()
} else {
destroyEffects()
}
}
private fun destroyEffects() {
if (config.increaseVolumeGradually) {
resetVolumeToInitialValue()
}
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
vibrator?.cancel()
vibrator = null
}
private fun snoozeAlarm(overrideSnoozeDuration: Int? = null) {
destroyEffects()
stopAlarmService()
if (overrideSnoozeDuration != null) {
scheduleSnoozedAlarm(overrideSnoozeDuration)
} else if (config.useSameSnooze) {
@@ -346,18 +234,22 @@ class ReminderActivity : SimpleActivity() {
}
private fun scheduleSnoozedAlarm(snoozeMinutes: Int) {
setupAlarmClock(
alarm = alarm!!,
triggerTimeMillis = Calendar.getInstance()
.apply { add(Calendar.MINUTE, snoozeMinutes) }
.timeInMillis
)
if (alarm != null) {
setupAlarmClock(
alarm = alarm!!,
triggerTimeMillis = Calendar.getInstance()
.apply { add(Calendar.MINUTE, snoozeMinutes) }
.timeInMillis
)
wasAlarmSnoozed = true
}
wasAlarmSnoozed = true
finishActivity()
}
private fun finishActivity() {
stopAlarmService()
if (!wasAlarmSnoozed && alarm != null) {
cancelAlarmClock(alarm!!)
if (alarm!!.days > 0) {
@@ -367,8 +259,6 @@ class ReminderActivity : SimpleActivity() {
disableExpiredAlarm(alarm!!)
}
finished = true
destroyEffects()
finish()
overridePendingTransition(0, 0)
}
@@ -386,8 +276,4 @@ class ReminderActivity : SimpleActivity() {
setTurnScreenOn(true)
}
}
private fun cancelNotification() {
notificationManager.cancel(ALARM_NOTIF_ID)
}
}

View File

@@ -4,8 +4,8 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.hideNotification
import org.fossify.clock.extensions.setupAlarmClock
import org.fossify.clock.extensions.stopAlarmService
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.commons.extensions.showPickSecondsDialog
import org.fossify.commons.helpers.MINUTE_SECONDS
@@ -16,7 +16,7 @@ class SnoozeReminderActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
val id = intent.getIntExtra(ALARM_ID, -1)
val alarm = dbHelper.getAlarmWithId(id) ?: return
hideNotification(id)
stopAlarmService()
showPickSecondsDialog(
curSeconds = config.snoozeTime * MINUTE_SECONDS,
isSnoozePicker = true,

View File

@@ -14,7 +14,6 @@ import android.media.AudioManager.STREAM_ALARM
import android.media.RingtoneManager
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.text.SpannableString
import android.text.style.RelativeSizeSpan
import android.widget.Toast
@@ -52,7 +51,6 @@ import org.fossify.clock.helpers.formatTime
import org.fossify.clock.helpers.getAllTimeZones
import org.fossify.clock.helpers.getCurrentDayMinutes
import org.fossify.clock.helpers.getDefaultTimeZoneTitle
import org.fossify.clock.helpers.getPassedSeconds
import org.fossify.clock.helpers.getTimeOfNextAlarm
import org.fossify.clock.interfaces.TimerDao
import org.fossify.clock.models.Alarm
@@ -65,6 +63,7 @@ import org.fossify.clock.receivers.DismissAlarmReceiver
import org.fossify.clock.receivers.EarlyAlarmDismissalReceiver
import org.fossify.clock.receivers.HideAlarmReceiver
import org.fossify.clock.receivers.HideTimerReceiver
import org.fossify.clock.services.AlarmService
import org.fossify.clock.services.SnoozeService
import org.fossify.commons.extensions.formatMinutesToTimeString
import org.fossify.commons.extensions.formatSecondsToTimeString
@@ -360,23 +359,6 @@ fun Context.rescheduleEnabledAlarms() {
}
}
fun Context.isScreenOn() = (getSystemService(Context.POWER_SERVICE) as PowerManager).isScreenOn
fun Context.showAlarmNotification(alarm: Alarm) {
val pendingIntent = getOpenAlarmTabIntent()
val notification = getAlarmNotification(pendingIntent, alarm)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
try {
notificationManager.notify(alarm.id, notification)
} catch (e: Exception) {
showErrorToast(e)
}
if (alarm.days > 0) {
scheduleNextAlarm(alarm, false)
}
}
fun Context.getTimerNotification(timer: Timer, pendingIntent: PendingIntent, addDeleteIntent: Boolean): Notification {
var soundUri = timer.soundUri
if (soundUri == SILENT) {
@@ -481,68 +463,6 @@ fun Context.getDismissAlarmPendingIntent(alarmId: Int, notificationId: Int): Pen
return PendingIntent.getBroadcast(this, alarmId, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
fun Context.getAlarmNotification(pendingIntent: PendingIntent, alarm: Alarm): Notification {
val soundUri = alarm.soundUri
if (soundUri != SILENT) {
grantReadUriPermission(soundUri)
}
val channelId = "simple_alarm_channel_${soundUri}_${alarm.vibrate}"
val label = alarm.label.ifEmpty {
getString(org.fossify.commons.R.string.alarm)
}
if (isOreoPlus()) {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setLegacyStreamType(STREAM_ALARM)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build()
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val importance = NotificationManager.IMPORTANCE_HIGH
NotificationChannel(channelId, label, importance).apply {
setBypassDnd(true)
enableLights(true)
lightColor = getProperPrimaryColor()
enableVibration(alarm.vibrate)
setSound(soundUri.toUri(), audioAttributes)
notificationManager.createNotificationChannel(this)
}
}
val dismissIntent = getHideAlarmPendingIntent(alarm, channelId)
val builder = NotificationCompat.Builder(this)
.setContentTitle(label)
.setContentText(getFormattedTime(getPassedSeconds(), false, false))
.setSmallIcon(R.drawable.ic_alarm_vector)
.setContentIntent(pendingIntent)
.setPriority(Notification.PRIORITY_HIGH)
.setDefaults(Notification.DEFAULT_LIGHTS)
.setChannelId(channelId)
.addAction(
org.fossify.commons.R.drawable.ic_snooze_vector,
getString(org.fossify.commons.R.string.snooze),
getSnoozePendingIntent(alarm)
)
.addAction(org.fossify.commons.R.drawable.ic_cross_vector, getString(org.fossify.commons.R.string.dismiss), dismissIntent)
.setDeleteIntent(dismissIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
if (soundUri != SILENT) {
builder.setSound(soundUri.toUri(), STREAM_ALARM)
}
if (alarm.vibrate) {
val vibrateArray = LongArray(2) { 500 }
builder.setVibrate(vibrateArray)
}
val notification = builder.build()
notification.flags = notification.flags or Notification.FLAG_INSISTENT
return notification
}
fun Context.getSnoozePendingIntent(alarm: Alarm): PendingIntent {
val snoozeClass = if (config.useSameSnooze) SnoozeService::class.java else SnoozeReminderActivity::class.java
val intent = Intent(this, snoozeClass).setAction("Snooze")
@@ -613,4 +533,9 @@ fun Context.disableExpiredAlarm(alarm: Alarm) {
updateWidgets()
EventBus.getDefault().post(AlarmEvent.Refresh)
}
}
fun Context.stopAlarmService() {
val serviceIntent = Intent(this, AlarmService::class.java)
stopService(serviceIntent)
}

View File

@@ -1,31 +1,12 @@
package org.fossify.clock.receivers
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import org.fossify.clock.R
import org.fossify.clock.activities.ReminderActivity
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.disableExpiredAlarm
import org.fossify.clock.extensions.hideNotification
import org.fossify.clock.extensions.isScreenOn
import org.fossify.clock.extensions.showAlarmNotification
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
import org.fossify.clock.helpers.ALARM_NOTIF_ID
import org.fossify.clock.helpers.EARLY_ALARM_NOTIF_ID
import org.fossify.commons.extensions.notificationManager
import org.fossify.clock.services.AlarmService
import org.fossify.commons.extensions.showErrorToast
import org.fossify.commons.helpers.isOreoPlus
@@ -33,69 +14,22 @@ class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getIntExtra(ALARM_ID, -1)
val alarm = context.dbHelper.getAlarmWithId(id) ?: return
if (id == -1) return
// Hide early dismissal notification if not already dismissed
context.hideNotification(EARLY_ALARM_NOTIF_ID)
if (context.isScreenOn()) {
context.showAlarmNotification(alarm)
Handler(Looper.getMainLooper()).postDelayed({
context.hideNotification(id)
context.disableExpiredAlarm(alarm)
}, context.config.alarmMaxReminderSecs * 1000L)
} else {
if (isOreoPlus()) {
val notificationManager = context.notificationManager
if (notificationManager.getNotificationChannel(ALARM_NOTIFICATION_CHANNEL_ID) == null) {
// cleans up previous notification channel that had sound properties
oldNotificationChannelCleanup(notificationManager)
NotificationChannel(
ALARM_NOTIFICATION_CHANNEL_ID,
"Alarm",
NotificationManager.IMPORTANCE_HIGH
).apply {
setBypassDnd(true)
setSound(null, null)
notificationManager.createNotificationChannel(this)
}
}
val reminderIntent = Intent(context, ReminderActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(ALARM_ID, id)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, reminderIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, ALARM_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_alarm_vector)
.setContentTitle(context.getString(org.fossify.commons.R.string.alarm))
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setFullScreenIntent(pendingIntent, true)
try {
notificationManager.notify(ALARM_NOTIF_ID, builder.build())
} catch (e: Exception) {
context.showErrorToast(e)
}
} else {
Intent(context, ReminderActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(ALARM_ID, id)
context.startActivity(this)
try {
Intent(context, AlarmService::class.java).apply {
putExtra(ALARM_ID, id)
if (isOreoPlus()) {
context.startForegroundService(this)
} else {
context.startService(this)
}
}
} catch (e: Exception) {
context.showErrorToast(e)
}
}
@RequiresApi(Build.VERSION_CODES.O)
private fun oldNotificationChannelCleanup(notificationManager: NotificationManager) {
notificationManager.deleteNotificationChannel("Alarm")
}
}

View File

@@ -3,10 +3,10 @@ package org.fossify.clock.receivers
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import org.fossify.clock.extensions.disableExpiredAlarm
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.deleteNotificationChannel
import org.fossify.clock.extensions.hideNotification
import org.fossify.clock.extensions.disableExpiredAlarm
import org.fossify.clock.extensions.stopAlarmService
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
import org.fossify.commons.helpers.ensureBackgroundThread
@@ -16,8 +16,7 @@ class HideAlarmReceiver : BroadcastReceiver() {
val id = intent.getIntExtra(ALARM_ID, -1)
val channelId = intent.getStringExtra(ALARM_NOTIFICATION_CHANNEL_ID)
channelId?.let { context.deleteNotificationChannel(channelId) }
context.hideNotification(id)
context.stopAlarmService()
ensureBackgroundThread {
val alarm = context.dbHelper.getAlarmWithId(id)
if (alarm != null) {

View File

@@ -0,0 +1,217 @@
package org.fossify.clock.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.Service
import android.content.Intent
import android.media.AudioAttributes
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Handler
import android.os.Looper
import android.os.VibrationEffect
import android.os.Vibrator
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import org.fossify.clock.R
import org.fossify.clock.activities.ReminderActivity
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.getFormattedTime
import org.fossify.clock.extensions.getHideAlarmPendingIntent
import org.fossify.clock.extensions.getSnoozePendingIntent
import org.fossify.clock.helpers.ALARM_ID
import org.fossify.clock.helpers.ALARM_NOTIFICATION_CHANNEL_ID
import org.fossify.clock.helpers.ALARM_NOTIF_ID
import org.fossify.clock.models.Alarm
import org.fossify.commons.extensions.notificationManager
import org.fossify.commons.helpers.SILENT
import org.fossify.commons.helpers.isOreoPlus
/**
* Service responsible for sounding the alarms and vibrations.
* It also shows a notification with actions to dismiss or snooze an alarm.
* Totally based on the previous implementation in the [ReminderActivity].
*/
class AlarmService : Service() {
companion object {
private const val DEFAULT_ALARM_VOLUME = 7
private const val INCREASE_VOLUME_DELAY = 300L
private const val MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS = 1
}
private var alarm: Alarm? = null
private var audioManager: AudioManager? = null
private var initialAlarmVolume = DEFAULT_ALARM_VOLUME
private var mediaPlayer: MediaPlayer? = null
private var vibrator: Vibrator? = null
private val autoDismissHandler = Handler(Looper.getMainLooper())
private val increaseVolumeHandler = Handler(Looper.getMainLooper())
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val alarmId = intent?.getIntExtra(ALARM_ID, -1) ?: -1
if (alarmId == -1) {
stopSelf()
return START_NOT_STICKY
}
alarm = applicationContext.dbHelper.getAlarmWithId(alarmId)
if (alarm == null) {
stopSelf()
return START_NOT_STICKY
}
val notification = buildNotification(alarm!!)
startForeground(ALARM_NOTIF_ID, notification)
startAlarmEffects(alarm!!)
startAutoDismiss(config.alarmMaxReminderSecs)
return START_STICKY
}
private fun buildNotification(alarm: Alarm): Notification {
val channelId = ALARM_NOTIFICATION_CHANNEL_ID
if (isOreoPlus()) {
val channel = NotificationChannel(
channelId,
getString(org.fossify.commons.R.string.alarm),
NotificationManager.IMPORTANCE_HIGH
).apply {
setBypassDnd(true)
setSound(null, null)
}
notificationManager.createNotificationChannel(channel)
}
val contentTitle = if (alarm.label.isEmpty()) {
getString(org.fossify.commons.R.string.alarm)
} else {
alarm.label
}
val contentText = getFormattedTime(
passedSeconds = alarm.timeInMinutes * 60,
showSeconds = false,
makeAmPmSmaller = false
)
val reminderIntent = Intent(this, ReminderActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(ALARM_ID, alarm.id)
}
val pendingIntent = PendingIntent.getActivity(
this, 0, reminderIntent, FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE
)
val dismissIntent = applicationContext.getHideAlarmPendingIntent(alarm, channelId)
val snoozeIntent = applicationContext.getSnoozePendingIntent(alarm)
return NotificationCompat.Builder(this, channelId)
.setContentTitle(contentTitle)
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_alarm_vector)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setCategory(NotificationCompat.CATEGORY_ALARM)
.setDefaults(NotificationCompat.DEFAULT_LIGHTS)
.addAction(
org.fossify.commons.R.drawable.ic_snooze_vector,
getString(org.fossify.commons.R.string.snooze),
snoozeIntent
)
.addAction(
org.fossify.commons.R.drawable.ic_cross_vector,
getString(org.fossify.commons.R.string.dismiss),
dismissIntent
)
.setDeleteIntent(dismissIntent)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setFullScreenIntent(pendingIntent, true)
.build()
}
private fun startAlarmEffects(alarm: Alarm) {
if (alarm.soundUri != SILENT) {
try {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setFlags(AudioAttributes.FLAG_AUDIBILITY_ENFORCED)
.build()
mediaPlayer = MediaPlayer().apply {
setAudioAttributes(audioAttributes)
setDataSource(this@AlarmService, alarm.soundUri.toUri())
isLooping = true
prepare()
start()
}
if (config.increaseVolumeGradually) {
initialAlarmVolume = audioManager?.getStreamVolume(
AudioManager.STREAM_ALARM
) ?: DEFAULT_ALARM_VOLUME
scheduleVolumeIncrease(
lastVolume = MIN_ALARM_VOLUME_FOR_INCREASING_ALARMS.toFloat(),
maxVolume = initialAlarmVolume.toFloat(),
delay = 0
)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
if (alarm.vibrate && isOreoPlus()) {
vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator
val pattern = longArrayOf(500, 500)
vibrator?.vibrate(VibrationEffect.createWaveform(pattern, 0))
}
}
private fun scheduleVolumeIncrease(lastVolume: Float, maxVolume: Float, delay: Long) {
increaseVolumeHandler.postDelayed({
val newVolume = (lastVolume + 0.1f).coerceAtMost(maxVolume)
audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, newVolume.toInt(), 0)
if (newVolume < maxVolume) {
scheduleVolumeIncrease(newVolume, maxVolume, INCREASE_VOLUME_DELAY)
}
}, delay)
}
private fun resetVolumeToInitialValue() {
if (config.increaseVolumeGradually) {
audioManager?.setStreamVolume(AudioManager.STREAM_ALARM, initialAlarmVolume, 0)
}
}
private fun startAutoDismiss(durationSecs: Int) {
autoDismissHandler.postDelayed({
stopSelf()
}, durationSecs * 1000L)
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
vibrator?.cancel()
vibrator = null
// Clear any scheduled volume changes or auto-dismiss messages
increaseVolumeHandler.removeCallbacksAndMessages(null)
autoDismissHandler.removeCallbacksAndMessages(null)
resetVolumeToInitialValue()
}
override fun onBind(intent: Intent?) = null
}

View File

@@ -4,8 +4,8 @@ import android.app.IntentService
import android.content.Intent
import org.fossify.clock.extensions.config
import org.fossify.clock.extensions.dbHelper
import org.fossify.clock.extensions.hideNotification
import org.fossify.clock.extensions.setupAlarmClock
import org.fossify.clock.extensions.stopAlarmService
import org.fossify.clock.helpers.ALARM_ID
import java.util.Calendar
@@ -13,7 +13,7 @@ class SnoozeService : IntentService("Snooze") {
override fun onHandleIntent(intent: Intent?) {
val id = intent!!.getIntExtra(ALARM_ID, -1)
val alarm = dbHelper.getAlarmWithId(id) ?: return
hideNotification(id)
stopAlarmService()
setupAlarmClock(
alarm = alarm,
triggerTimeMillis = Calendar.getInstance()