mirror of
https://github.com/FossifyOrg/Clock.git
synced 2026-01-19 05:07:55 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
217
app/src/main/kotlin/org/fossify/clock/services/AlarmService.kt
Normal file
217
app/src/main/kotlin/org/fossify/clock/services/AlarmService.kt
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user