fix(alarms): harden native playback and pre-notice actions
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
|
||||
|
||||
@@ -66,19 +67,24 @@
|
||||
|
||||
<receiver
|
||||
android:name=".PluriWaveAlarmReceiver"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.FIRE"/>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.PRE_NOTICE"/>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.SKIP_NEXT"/>
|
||||
<action android:name="es.freetimelab.pluriwave.alarm.POSTPONE_NEXT"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".PluriWaveBootReceiver"
|
||||
android:exported="true">
|
||||
android:exported="true"
|
||||
android:directBootAware="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.intent.action.USER_UNLOCKED"/>
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
|
||||
<action android:name="android.intent.action.TIME_SET"/>
|
||||
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
|
||||
|
||||
@@ -7,12 +7,16 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
|
||||
class AlarmScheduler(private val context: Context) {
|
||||
private val tag = "PluriWave"
|
||||
private val appContext = context.applicationContext
|
||||
private val alarmManager =
|
||||
context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
appContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
|
||||
fun scheduleAlarm(
|
||||
id: String,
|
||||
@@ -22,95 +26,145 @@ class AlarmScheduler(private val context: Context) {
|
||||
stationName: String?,
|
||||
stationUrl: String?,
|
||||
fallbackSound: String?,
|
||||
volume: Float
|
||||
volume: Float,
|
||||
hour: Int? = null,
|
||||
minute: Int? = null,
|
||||
scheduleType: String? = null,
|
||||
weekdays: List<Int> = emptyList(),
|
||||
oneShotDateMillis: Long? = null,
|
||||
snoozeUntilMillis: Long? = null,
|
||||
snoozeOriginMillis: Long? = null,
|
||||
lastHandledAtMillis: Long? = null,
|
||||
soundOnVacation: Boolean = true,
|
||||
snoozeMinutes: Int = 5
|
||||
): Boolean {
|
||||
val existing = readSpec(id)
|
||||
val preservedSnooze = preserveNativeSnooze(
|
||||
existing = existing,
|
||||
requestedTriggerAtMillis = triggerAtMillis,
|
||||
requestedSnoozeUntilMillis = snoozeUntilMillis
|
||||
)
|
||||
val spec = NativeAlarmSpec(
|
||||
id = id,
|
||||
title = title,
|
||||
enabled = true,
|
||||
triggerAtMillis = triggerAtMillis,
|
||||
preNoticeAtMillis = preNoticeAtMillis,
|
||||
hour = hour ?: localHour(triggerAtMillis),
|
||||
minute = minute ?: localMinute(triggerAtMillis),
|
||||
scheduleType = scheduleType ?: SCHEDULE_UNICA,
|
||||
weekdays = weekdays,
|
||||
oneShotDateMillis = oneShotDateMillis,
|
||||
snoozeUntilMillis = preservedSnooze?.first ?: snoozeUntilMillis,
|
||||
snoozeOriginMillis = preservedSnooze?.second ?: snoozeOriginMillis,
|
||||
lastHandledAtMillis = lastHandledAtMillis,
|
||||
soundOnVacation = soundOnVacation,
|
||||
snoozeMinutes = sanitizeSnoozeMinutes(snoozeMinutes),
|
||||
stationName = stationName,
|
||||
stationUrl = stationUrl,
|
||||
fallbackSound = fallbackSound,
|
||||
volume = volume.coerceIn(0f, 1f),
|
||||
timezoneId = TimeZone.getDefault().id
|
||||
)
|
||||
return scheduleSpec(spec, persistOnSuccess = true)
|
||||
}
|
||||
|
||||
private fun scheduleSpec(spec: NativeAlarmSpec, persistOnSuccess: Boolean): Boolean {
|
||||
val nextTrigger = computeNextTriggerMillis(spec)
|
||||
Log.d(
|
||||
tag,
|
||||
"alarm.schedule id=$id title=$title triggerAtMillis=$triggerAtMillis preNoticeAtMillis=$preNoticeAtMillis canExact=${canScheduleExactAlarms()}"
|
||||
"alarm.schedule id=${spec.id} title=${spec.title} trigger=$nextTrigger type=${spec.scheduleType} snooze=${spec.snoozeUntilMillis} canExact=${canScheduleExactAlarms()}"
|
||||
)
|
||||
val alarmIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(id, 1),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_FIRE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_STATION_NAME, stationName)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_STATION_URL, stationUrl)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_FALLBACK_SOUND, fallbackSound)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_VOLUME, volume)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val showIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
requestCode(id, 2),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION, PluriWaveAlarmReceiver.ACTION_FIRE)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val mainScheduled = scheduleMainAlarm(id, triggerAtMillis, showIntent, alarmIntent)
|
||||
if (mainScheduled) {
|
||||
saveScheduledAlarm(
|
||||
id,
|
||||
title,
|
||||
triggerAtMillis,
|
||||
preNoticeAtMillis,
|
||||
stationName,
|
||||
stationUrl,
|
||||
fallbackSound,
|
||||
volume
|
||||
)
|
||||
} else {
|
||||
removeScheduledAlarm(id)
|
||||
if (nextTrigger == null) {
|
||||
Log.d(tag, "alarm.schedule no next trigger id=${spec.id}")
|
||||
removeScheduledAlarm(spec.id)
|
||||
cancelPending("fire", pendingFireIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||
cancelPending("show", pendingShowIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||
return true
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val scheduledSpec = spec.copy(
|
||||
triggerAtMillis = nextTrigger,
|
||||
preNoticeAtMillis = if (spec.snoozeUntilMillis == null) {
|
||||
nextTrigger - PRE_NOTICE_MILLIS
|
||||
} else {
|
||||
0L
|
||||
}
|
||||
)
|
||||
val alarmIntent = fireIntent(scheduledSpec)
|
||||
val showIntent = showIntent(scheduledSpec)
|
||||
|
||||
val mainScheduled = scheduleMainAlarm(
|
||||
scheduledSpec.id,
|
||||
scheduledSpec.triggerAtMillis,
|
||||
showIntent,
|
||||
alarmIntent
|
||||
)
|
||||
if (!mainScheduled) {
|
||||
Log.w(tag, "alarm.schedule main alarm fallback failed or degraded id=$id")
|
||||
Log.w(tag, "alarm.schedule main failed but keeping spec for future resync id=${scheduledSpec.id}")
|
||||
saveScheduledAlarm(scheduledSpec)
|
||||
return false
|
||||
}
|
||||
|
||||
if (preNoticeAtMillis > now) {
|
||||
if (persistOnSuccess) {
|
||||
saveScheduledAlarm(scheduledSpec)
|
||||
}
|
||||
schedulePreNotice(scheduledSpec)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun schedulePreNotice(spec: NativeAlarmSpec) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (spec.snoozeUntilMillis != null) {
|
||||
cancelPending("preNotice", pendingPreNoticeIntent(spec.id, PendingIntent.FLAG_NO_CREATE))
|
||||
Log.d(tag, "alarm.schedule preNotice skipped for snooze id=${spec.id}")
|
||||
return
|
||||
}
|
||||
if (spec.preNoticeAtMillis > now) {
|
||||
try {
|
||||
alarmManager.setExactAndAllowWhileIdle(
|
||||
AlarmManager.RTC_WAKEUP,
|
||||
preNoticeAtMillis,
|
||||
spec.preNoticeAtMillis,
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(id, 3),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
appContext,
|
||||
requestCode(spec.id, 3),
|
||||
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, spec.id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, spec.title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, spec.snoozeMinutes)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, spec.triggerAtMillis)
|
||||
putExtra(
|
||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
Log.d(tag, "alarm.schedule preNotice OK id=$id")
|
||||
Log.d(tag, "alarm.schedule preNotice OK id=${spec.id}")
|
||||
} catch (_: SecurityException) {
|
||||
// The main alarm is already scheduled with setAlarmClock.
|
||||
Log.w(tag, "alarm.schedule preNotice SecurityException id=$id")
|
||||
Log.w(tag, "alarm.schedule preNotice SecurityException id=${spec.id}")
|
||||
}
|
||||
} else if (triggerAtMillis > now) {
|
||||
context.sendBroadcast(
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
} else if (spec.triggerAtMillis > now) {
|
||||
appContext.sendBroadcast(
|
||||
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, spec.id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, spec.title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, spec.snoozeMinutes)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, spec.triggerAtMillis)
|
||||
putExtra(
|
||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
)
|
||||
}
|
||||
)
|
||||
Log.d(tag, "alarm.schedule preNotice immediate id=$id")
|
||||
Log.d(tag, "alarm.schedule preNotice immediate id=${spec.id}")
|
||||
} else {
|
||||
Log.d(tag, "alarm.schedule preNotice skipped id=$id")
|
||||
Log.d(tag, "alarm.schedule preNotice skipped id=${spec.id}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun scheduleMainAlarm(
|
||||
@@ -141,10 +195,7 @@ class AlarmScheduler(private val context: Context) {
|
||||
)
|
||||
Log.d(tag, "alarm.schedule setExactAndAllowWhileIdle fallback OK id=$id")
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
Log.e(
|
||||
tag,
|
||||
"alarm.schedule exact permission missing; refusing inexact fallback id=$id"
|
||||
)
|
||||
Log.e(tag, "alarm.schedule exact permission missing; refusing inexact fallback id=$id")
|
||||
return false
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
alarmManager.setAndAllowWhileIdle(
|
||||
@@ -168,22 +219,88 @@ class AlarmScheduler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun onAlarmFired(id: String) {
|
||||
val spec = readSpec(id) ?: return
|
||||
val firedAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
val next = spec.copy(
|
||||
snoozeUntilMillis = null,
|
||||
snoozeOriginMillis = null,
|
||||
lastHandledAtMillis = firedAt,
|
||||
enabled = spec.scheduleType != SCHEDULE_UNICA
|
||||
)
|
||||
if (next.enabled) {
|
||||
scheduleSpec(next, persistOnSuccess = true)
|
||||
} else {
|
||||
removeScheduledAlarm(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun skipNext(id: String) {
|
||||
val spec = readSpec(id) ?: return
|
||||
val next = spec.copy(
|
||||
snoozeUntilMillis = null,
|
||||
snoozeOriginMillis = null,
|
||||
lastHandledAtMillis = spec.triggerAtMillis,
|
||||
enabled = spec.scheduleType != SCHEDULE_UNICA
|
||||
)
|
||||
if (next.enabled) {
|
||||
scheduleSpec(next, persistOnSuccess = true)
|
||||
} else {
|
||||
cancelAlarm(id)
|
||||
}
|
||||
}
|
||||
|
||||
fun snooze(id: String, minutes: Int) {
|
||||
val spec = readSpec(id) ?: return
|
||||
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
||||
val snoozeUntil = System.currentTimeMillis() + safeMinutes * 60_000L
|
||||
scheduleSpec(
|
||||
spec.copy(
|
||||
snoozeUntilMillis = snoozeUntil,
|
||||
snoozeOriginMillis = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
),
|
||||
persistOnSuccess = true
|
||||
)
|
||||
}
|
||||
|
||||
fun postponeNext(id: String, minutes: Int): Long? {
|
||||
val spec = readSpec(id) ?: return null
|
||||
val safeMinutes = sanitizeSnoozeMinutes(minutes)
|
||||
val occurrenceAt = spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
val target = occurrenceAt + safeMinutes * 60_000L
|
||||
val now = System.currentTimeMillis()
|
||||
val snoozeUntil = if (target > now) target else now + safeMinutes * 60_000L
|
||||
Log.d(
|
||||
tag,
|
||||
"alarm.postponeNext id=$id minutes=$safeMinutes occurrence=$occurrenceAt target=$snoozeUntil"
|
||||
)
|
||||
scheduleSpec(
|
||||
spec.copy(
|
||||
snoozeUntilMillis = snoozeUntil,
|
||||
snoozeOriginMillis = occurrenceAt,
|
||||
snoozeMinutes = safeMinutes
|
||||
),
|
||||
persistOnSuccess = true
|
||||
)
|
||||
return occurrenceAt
|
||||
}
|
||||
|
||||
fun cancelAlarm(id: String) {
|
||||
Log.d(tag, "alarm.cancel id=$id")
|
||||
removeScheduledAlarm(id)
|
||||
cancelPending("fire", pendingFireIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||
cancelPending("show", pendingShowIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||
cancelPending("preNotice", pendingPreNoticeIntent(id, PendingIntent.FLAG_NO_CREATE))
|
||||
NotificationManagerCompat.from(context).cancel(
|
||||
NotificationManagerCompat.from(appContext).cancel(
|
||||
PluriWaveAlarmReceiver.notificationIdForAlarm(id)
|
||||
)
|
||||
NotificationManagerCompat.from(context).cancel(
|
||||
NotificationManagerCompat.from(appContext).cancel(
|
||||
PluriWaveAlarmReceiver.fireNotificationIdForAlarm(id)
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissFireNotification(id: String) {
|
||||
NotificationManagerCompat.from(context).cancel(
|
||||
NotificationManagerCompat.from(appContext).cancel(
|
||||
PluriWaveAlarmReceiver.fireNotificationIdForAlarm(id)
|
||||
)
|
||||
}
|
||||
@@ -194,27 +311,10 @@ class AlarmScheduler(private val context: Context) {
|
||||
}
|
||||
|
||||
fun reschedulePersistedAlarms() {
|
||||
val now = System.currentTimeMillis()
|
||||
for (id in prefs().getStringSet(KEY_IDS, emptySet()).orEmpty()) {
|
||||
val raw = prefs().getString("$KEY_ALARM_PREFIX$id", null) ?: continue
|
||||
val spec = readSpec(id) ?: continue
|
||||
try {
|
||||
val data = JSONObject(raw)
|
||||
val triggerAt = data.optLong("triggerAtMillis", 0L)
|
||||
if (triggerAt <= now) {
|
||||
Log.d(tag, "alarm.reschedule skip stale id=$id triggerAt=$triggerAt")
|
||||
removeScheduledAlarm(id)
|
||||
continue
|
||||
}
|
||||
scheduleAlarm(
|
||||
id = id,
|
||||
title = data.optString("title", "PluriWave"),
|
||||
triggerAtMillis = triggerAt,
|
||||
preNoticeAtMillis = data.optLong("preNoticeAtMillis", 0L),
|
||||
stationName = data.optString("stationName").takeIf { it.isNotBlank() },
|
||||
stationUrl = data.optString("stationUrl").takeIf { it.isNotBlank() },
|
||||
fallbackSound = data.optString("fallbackSound").takeIf { it.isNotBlank() },
|
||||
volume = data.optDouble("volume", 0.85).toFloat()
|
||||
)
|
||||
scheduleSpec(spec, persistOnSuccess = true)
|
||||
Log.d(tag, "alarm.reschedule OK id=$id")
|
||||
} catch (error: Throwable) {
|
||||
Log.e(tag, "alarm.reschedule failed id=$id", error)
|
||||
@@ -222,33 +322,113 @@ class AlarmScheduler(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveScheduledAlarm(
|
||||
id: String,
|
||||
title: String,
|
||||
triggerAtMillis: Long,
|
||||
preNoticeAtMillis: Long,
|
||||
stationName: String?,
|
||||
stationUrl: String?,
|
||||
fallbackSound: String?,
|
||||
volume: Float
|
||||
) {
|
||||
val ids = prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().toMutableSet()
|
||||
ids.add(id)
|
||||
val data = JSONObject().apply {
|
||||
put("title", title)
|
||||
put("triggerAtMillis", triggerAtMillis)
|
||||
put("preNoticeAtMillis", preNoticeAtMillis)
|
||||
put("stationName", stationName)
|
||||
put("stationUrl", stationUrl)
|
||||
put("fallbackSound", fallbackSound)
|
||||
put("volume", volume)
|
||||
fun pendingAlarmCount(): Int =
|
||||
prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().size
|
||||
|
||||
private fun preserveNativeSnooze(
|
||||
existing: NativeAlarmSpec?,
|
||||
requestedTriggerAtMillis: Long,
|
||||
requestedSnoozeUntilMillis: Long?
|
||||
): Pair<Long, Long>? {
|
||||
if (requestedSnoozeUntilMillis != null || existing == null) return null
|
||||
val snoozeUntil = existing.snoozeUntilMillis ?: return null
|
||||
val snoozeOrigin = existing.snoozeOriginMillis ?: return null
|
||||
if (snoozeUntil <= System.currentTimeMillis()) return null
|
||||
if (snoozeOrigin != requestedTriggerAtMillis) return null
|
||||
Log.d(
|
||||
tag,
|
||||
"alarm.schedule preserving native snooze id=${existing.id} origin=$snoozeOrigin until=$snoozeUntil"
|
||||
)
|
||||
return snoozeUntil to snoozeOrigin
|
||||
}
|
||||
|
||||
private fun computeNextTriggerMillis(spec: NativeAlarmSpec): Long? {
|
||||
val now = System.currentTimeMillis()
|
||||
spec.snoozeUntilMillis?.let { if (it > now) return it }
|
||||
if (!spec.enabled) return null
|
||||
val base = maxOf(now, (spec.lastHandledAtMillis ?: 0L) + 60_000L)
|
||||
return when (spec.scheduleType) {
|
||||
SCHEDULE_UNICA -> computeOneShot(spec, base)
|
||||
SCHEDULE_DIAS_SEMANA -> computeWeekday(spec, base)
|
||||
else -> computeDaily(spec, base)
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeOneShot(spec: NativeAlarmSpec, baseMillis: Long): Long? {
|
||||
val candidate = Calendar.getInstance().apply {
|
||||
timeInMillis = spec.oneShotDateMillis ?: spec.triggerAtMillis
|
||||
set(Calendar.HOUR_OF_DAY, spec.hour)
|
||||
set(Calendar.MINUTE, spec.minute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
return candidate.timeInMillis.takeIf { it > baseMillis }
|
||||
}
|
||||
|
||||
private fun computeDaily(spec: NativeAlarmSpec, baseMillis: Long): Long? {
|
||||
val candidate = Calendar.getInstance().apply {
|
||||
timeInMillis = baseMillis
|
||||
set(Calendar.HOUR_OF_DAY, spec.hour)
|
||||
set(Calendar.MINUTE, spec.minute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
if (candidate.timeInMillis <= baseMillis) {
|
||||
candidate.add(Calendar.DAY_OF_YEAR, 1)
|
||||
}
|
||||
return candidate.timeInMillis
|
||||
}
|
||||
|
||||
private fun computeWeekday(spec: NativeAlarmSpec, baseMillis: Long): Long? {
|
||||
if (spec.weekdays.isEmpty()) return null
|
||||
val candidate = Calendar.getInstance().apply {
|
||||
timeInMillis = baseMillis
|
||||
set(Calendar.HOUR_OF_DAY, spec.hour)
|
||||
set(Calendar.MINUTE, spec.minute)
|
||||
set(Calendar.SECOND, 0)
|
||||
set(Calendar.MILLISECOND, 0)
|
||||
}
|
||||
for (i in 0 until 370) {
|
||||
if (candidate.timeInMillis > baseMillis &&
|
||||
spec.weekdays.contains(dartWeekday(candidate))
|
||||
) {
|
||||
return candidate.timeInMillis
|
||||
}
|
||||
candidate.add(Calendar.DAY_OF_YEAR, 1)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun dartWeekday(calendar: Calendar): Int =
|
||||
when (calendar.get(Calendar.DAY_OF_WEEK)) {
|
||||
Calendar.MONDAY -> 1
|
||||
Calendar.TUESDAY -> 2
|
||||
Calendar.WEDNESDAY -> 3
|
||||
Calendar.THURSDAY -> 4
|
||||
Calendar.FRIDAY -> 5
|
||||
Calendar.SATURDAY -> 6
|
||||
else -> 7
|
||||
}
|
||||
|
||||
private fun saveScheduledAlarm(spec: NativeAlarmSpec) {
|
||||
val ids = prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().toMutableSet()
|
||||
ids.add(spec.id)
|
||||
prefs().edit()
|
||||
.putStringSet(KEY_IDS, ids)
|
||||
.putString("$KEY_ALARM_PREFIX$id", data.toString())
|
||||
.putString("$KEY_ALARM_PREFIX${spec.id}", spec.toJson().toString())
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun readSpec(id: String): NativeAlarmSpec? {
|
||||
val raw = prefs().getString("$KEY_ALARM_PREFIX$id", null) ?: return null
|
||||
return try {
|
||||
NativeAlarmSpec.fromJson(JSONObject(raw))
|
||||
} catch (error: Throwable) {
|
||||
Log.e(tag, "alarm.readSpec failed id=$id", error)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeScheduledAlarm(id: String) {
|
||||
val ids = prefs().getStringSet(KEY_IDS, emptySet()).orEmpty().toMutableSet()
|
||||
ids.remove(id)
|
||||
@@ -258,7 +438,9 @@ class AlarmScheduler(private val context: Context) {
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun prefs() = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
private fun prefs() =
|
||||
appContext.createDeviceProtectedStorageContext()
|
||||
.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
|
||||
private fun cancelPending(name: String, pendingIntent: PendingIntent?) {
|
||||
if (pendingIntent == null) {
|
||||
@@ -270,11 +452,52 @@ class AlarmScheduler(private val context: Context) {
|
||||
Log.d(tag, "alarm.cancel $name OK")
|
||||
}
|
||||
|
||||
private fun fireIntent(spec: NativeAlarmSpec): PendingIntent =
|
||||
PendingIntent.getBroadcast(
|
||||
appContext,
|
||||
requestCode(spec.id, 1),
|
||||
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_FIRE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, spec.id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, spec.title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_STATION_NAME, spec.stationName)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_STATION_URL, spec.stationUrl)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_FALLBACK_SOUND, spec.fallbackSound)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_VOLUME, spec.volume)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, spec.triggerAtMillis)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, spec.snoozeMinutes)
|
||||
putExtra(
|
||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun showIntent(spec: NativeAlarmSpec): PendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
appContext,
|
||||
requestCode(spec.id, 2),
|
||||
Intent(appContext, MainActivity::class.java).apply {
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, spec.id)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, spec.title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION, PluriWaveAlarmReceiver.ACTION_FIRE)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, spec.triggerAtMillis)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, spec.snoozeMinutes)
|
||||
putExtra(
|
||||
PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT,
|
||||
spec.snoozeOriginMillis ?: spec.triggerAtMillis
|
||||
)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun pendingFireIntent(id: String, flags: Int): PendingIntent? =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
appContext,
|
||||
requestCode(id, 1),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_FIRE
|
||||
},
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
@@ -282,10 +505,10 @@ class AlarmScheduler(private val context: Context) {
|
||||
|
||||
private fun pendingShowIntent(id: String, flags: Int): PendingIntent? =
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
appContext,
|
||||
requestCode(id, 2),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
Intent(appContext, MainActivity::class.java).apply {
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION, PluriWaveAlarmReceiver.ACTION_FIRE)
|
||||
},
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
@@ -293,19 +516,113 @@ class AlarmScheduler(private val context: Context) {
|
||||
|
||||
private fun pendingPreNoticeIntent(id: String, flags: Int): PendingIntent? =
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
appContext,
|
||||
requestCode(id, 3),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
Intent(appContext, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = PluriWaveAlarmReceiver.ACTION_PRE_NOTICE
|
||||
},
|
||||
flags or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun localHour(millis: Long): Int =
|
||||
Calendar.getInstance().apply { timeInMillis = millis }.get(Calendar.HOUR_OF_DAY)
|
||||
|
||||
private fun localMinute(millis: Long): Int =
|
||||
Calendar.getInstance().apply { timeInMillis = millis }.get(Calendar.MINUTE)
|
||||
|
||||
private fun sanitizeSnoozeMinutes(minutes: Int): Int =
|
||||
if (minutes == 3 || minutes == 5 || minutes == 10) minutes else 5
|
||||
|
||||
private fun requestCode(id: String, slot: Int): Int = 31 * id.hashCode() + slot
|
||||
|
||||
private data class NativeAlarmSpec(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val enabled: Boolean,
|
||||
val triggerAtMillis: Long,
|
||||
val preNoticeAtMillis: Long,
|
||||
val hour: Int,
|
||||
val minute: Int,
|
||||
val scheduleType: String,
|
||||
val weekdays: List<Int>,
|
||||
val oneShotDateMillis: Long?,
|
||||
val snoozeUntilMillis: Long?,
|
||||
val snoozeOriginMillis: Long?,
|
||||
val lastHandledAtMillis: Long?,
|
||||
val soundOnVacation: Boolean,
|
||||
val snoozeMinutes: Int,
|
||||
val stationName: String?,
|
||||
val stationUrl: String?,
|
||||
val fallbackSound: String?,
|
||||
val volume: Float,
|
||||
val timezoneId: String
|
||||
) {
|
||||
fun toJson(): JSONObject = JSONObject().apply {
|
||||
put("schemaVersion", 2)
|
||||
put("id", id)
|
||||
put("title", title)
|
||||
put("enabled", enabled)
|
||||
put("triggerAtMillis", triggerAtMillis)
|
||||
put("preNoticeAtMillis", preNoticeAtMillis)
|
||||
put("hour", hour)
|
||||
put("minute", minute)
|
||||
put("scheduleType", scheduleType)
|
||||
put("weekdays", JSONArray(weekdays))
|
||||
put("oneShotDateMillis", oneShotDateMillis)
|
||||
put("snoozeUntilMillis", snoozeUntilMillis)
|
||||
put("snoozeOriginMillis", snoozeOriginMillis)
|
||||
put("lastHandledAtMillis", lastHandledAtMillis)
|
||||
put("soundOnVacation", soundOnVacation)
|
||||
put("snoozeMinutes", snoozeMinutes)
|
||||
put("stationName", stationName)
|
||||
put("stationUrl", stationUrl)
|
||||
put("fallbackSound", fallbackSound)
|
||||
put("volume", volume)
|
||||
put("timezoneId", timezoneId)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: JSONObject): NativeAlarmSpec {
|
||||
val weekdaysJson = json.optJSONArray("weekdays") ?: JSONArray()
|
||||
return NativeAlarmSpec(
|
||||
id = json.optString("id"),
|
||||
title = json.optString("title", "PluriWave"),
|
||||
enabled = json.optBoolean("enabled", true),
|
||||
triggerAtMillis = json.optLong("triggerAtMillis", 0L),
|
||||
preNoticeAtMillis = json.optLong("preNoticeAtMillis", 0L),
|
||||
hour = json.optInt("hour", 7),
|
||||
minute = json.optInt("minute", 0),
|
||||
scheduleType = json.optString("scheduleType", SCHEDULE_UNICA),
|
||||
weekdays = (0 until weekdaysJson.length()).mapNotNull {
|
||||
weekdaysJson.optInt(it).takeIf { day -> day in 1..7 }
|
||||
},
|
||||
oneShotDateMillis = json.optNullableLong("oneShotDateMillis"),
|
||||
snoozeUntilMillis = json.optNullableLong("snoozeUntilMillis"),
|
||||
snoozeOriginMillis = json.optNullableLong("snoozeOriginMillis"),
|
||||
lastHandledAtMillis = json.optNullableLong("lastHandledAtMillis"),
|
||||
soundOnVacation = json.optBoolean("soundOnVacation", true),
|
||||
snoozeMinutes = json.optInt("snoozeMinutes", 5).let {
|
||||
if (it == 3 || it == 5 || it == 10) it else 5
|
||||
},
|
||||
stationName = json.optString("stationName").takeIf { it.isNotBlank() },
|
||||
stationUrl = json.optString("stationUrl").takeIf { it.isNotBlank() },
|
||||
fallbackSound = json.optString("fallbackSound").takeIf { it.isNotBlank() },
|
||||
volume = json.optDouble("volume", 0.85).toFloat(),
|
||||
timezoneId = json.optString("timezoneId", TimeZone.getDefault().id)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val PREFS = "pluriwave_alarm_scheduler"
|
||||
private const val KEY_IDS = "scheduled_alarm_ids"
|
||||
private const val KEY_ALARM_PREFIX = "scheduled_alarm_"
|
||||
private const val PRE_NOTICE_MILLIS = 30 * 60 * 1000L
|
||||
private const val SCHEDULE_UNICA = "unica"
|
||||
private const val SCHEDULE_DIAS_SEMANA = "diasSemana"
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.optNullableLong(name: String): Long? =
|
||||
if (has(name) && !isNull(name)) optLong(name) else null
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package es.freetimelab.pluriwave
|
||||
|
||||
import android.Manifest
|
||||
import android.app.NotificationManager
|
||||
import android.content.ClipData
|
||||
import android.content.Intent
|
||||
import android.content.ActivityNotFoundException
|
||||
@@ -13,7 +14,9 @@ import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.FileProvider
|
||||
@@ -28,7 +31,8 @@ class MainActivity : AudioServiceActivity() {
|
||||
private val visualizerChannel = "pluriwave/audio_visualizer"
|
||||
private val alarmChannel = "pluriwave/alarm_scheduler"
|
||||
private val fileActionsChannel = "pluriwave/file_actions"
|
||||
private val permissionRequestCode = 4821
|
||||
private val visualizerPermissionRequestCode = 4821
|
||||
private val notificationPermissionRequestCode = 4822
|
||||
private var visualizer: Visualizer? = null
|
||||
private var pendingSink: EventChannel.EventSink? = null
|
||||
private var pendingArgs: Map<*, *>? = null
|
||||
@@ -70,6 +74,9 @@ class MainActivity : AudioServiceActivity() {
|
||||
val stationUrl = call.argument<String>("stationUrl")
|
||||
val fallbackSound = call.argument<String>("fallbackSound")
|
||||
val volume = call.argument<Number>("volume")?.toFloat() ?: 0.85f
|
||||
val weekdays =
|
||||
(call.argument<List<Int>>("weekdays") ?: emptyList())
|
||||
.filter { it in 1..7 }
|
||||
Log.d(tag, "alarm.channel scheduleAlarm id=$id triggerAtMillis=$triggerAtMillis preNoticeAtMillis=$preNoticeAtMillis")
|
||||
if (id == null || triggerAtMillis == null) {
|
||||
Log.w(tag, "alarm.channel scheduleAlarm invalid id=$id triggerAtMillis=$triggerAtMillis")
|
||||
@@ -83,7 +90,17 @@ class MainActivity : AudioServiceActivity() {
|
||||
stationName,
|
||||
stationUrl,
|
||||
fallbackSound,
|
||||
volume
|
||||
volume,
|
||||
hour = call.argument<Int>("hour"),
|
||||
minute = call.argument<Int>("minute"),
|
||||
scheduleType = call.argument<String>("scheduleType"),
|
||||
weekdays = weekdays,
|
||||
oneShotDateMillis = call.argument<Long>("oneShotDateMillis"),
|
||||
snoozeUntilMillis = call.argument<Long>("snoozeUntilMillis"),
|
||||
snoozeOriginMillis = call.argument<Long>("snoozeOriginMillis"),
|
||||
lastHandledAtMillis = call.argument<Long>("lastHandledAtMillis"),
|
||||
soundOnVacation = call.argument<Boolean>("soundOnVacation") ?: true,
|
||||
snoozeMinutes = call.argument<Int>("snoozeMinutes") ?: 5
|
||||
)
|
||||
result.success(scheduled)
|
||||
}
|
||||
@@ -119,12 +136,25 @@ class MainActivity : AudioServiceActivity() {
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"confirmFlutterAudio" -> {
|
||||
val id = call.argument<String>("id")
|
||||
Log.d(tag, "alarm.channel confirmFlutterAudio id=$id")
|
||||
if (id == null) {
|
||||
result.error("INVALID_ALARM", "Missing alarm id", null)
|
||||
} else {
|
||||
PluriWaveAlarmService.stop(this, id)
|
||||
result.success(null)
|
||||
}
|
||||
}
|
||||
"diagnostics" -> {
|
||||
Log.d(tag, "alarm.channel diagnostics")
|
||||
result.success(
|
||||
mapOf(
|
||||
"canScheduleExactAlarms" to alarmScheduler.canScheduleExactAlarms(),
|
||||
"notificationsEnabled" to NotificationManagerCompat.from(this).areNotificationsEnabled(),
|
||||
"canUseFullScreenIntent" to canUseFullScreenIntent(),
|
||||
"isIgnoringBatteryOptimizations" to isIgnoringBatteryOptimizations(),
|
||||
"nativePendingAlarmsCount" to alarmScheduler.pendingAlarmCount(),
|
||||
"manufacturer" to Build.MANUFACTURER,
|
||||
"sdkInt" to Build.VERSION.SDK_INT
|
||||
)
|
||||
@@ -134,6 +164,14 @@ class MainActivity : AudioServiceActivity() {
|
||||
Log.d(tag, "alarm.channel requestExactAlarmPermission")
|
||||
result.success(requestExactAlarmPermission())
|
||||
}
|
||||
"requestPostNotificationsPermission" -> {
|
||||
Log.d(tag, "alarm.channel requestPostNotificationsPermission")
|
||||
result.success(requestPostNotificationsPermission())
|
||||
}
|
||||
"requestFullScreenIntentPermission" -> {
|
||||
Log.d(tag, "alarm.channel requestFullScreenIntentPermission")
|
||||
result.success(requestFullScreenIntentPermission())
|
||||
}
|
||||
"getInitialAlarmIntent" -> {
|
||||
val payload = alarmPayload(intent)
|
||||
Log.d(tag, "alarm.channel getInitialAlarmIntent payload=$payload")
|
||||
@@ -203,7 +241,10 @@ class MainActivity : AudioServiceActivity() {
|
||||
return mapOf(
|
||||
"alarmId" to alarmId,
|
||||
"alarmTitle" to title,
|
||||
"alarmAction" to action
|
||||
"alarmAction" to action,
|
||||
"triggerAtMillis" to intent.getLongExtra(PluriWaveAlarmReceiver.EXTRA_TRIGGER_AT, 0L),
|
||||
"occurrenceAtMillis" to intent.getLongExtra(PluriWaveAlarmReceiver.EXTRA_OCCURRENCE_AT, 0L),
|
||||
"snoozeMinutes" to intent.getIntExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, 5)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -224,6 +265,47 @@ class MainActivity : AudioServiceActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPostNotificationsPermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true
|
||||
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
return true
|
||||
}
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
|
||||
notificationPermissionRequestCode
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun requestFullScreenIntentPermission(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return true
|
||||
if (canUseFullScreenIntent()) return true
|
||||
return try {
|
||||
startActivity(
|
||||
Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
|
||||
data = Uri.parse("package:$packageName")
|
||||
}
|
||||
)
|
||||
true
|
||||
} catch (error: Throwable) {
|
||||
Log.e(tag, "alarm.channel requestFullScreenIntentPermission failed", error)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun canUseFullScreenIntent(): Boolean {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) return true
|
||||
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
return manager.canUseFullScreenIntent()
|
||||
}
|
||||
|
||||
private fun isIgnoringBatteryOptimizations(): Boolean {
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
return powerManager.isIgnoringBatteryOptimizations(packageName)
|
||||
}
|
||||
|
||||
private fun openDirectory(path: String): Boolean {
|
||||
val folder = File(path)
|
||||
if (!folder.exists()) {
|
||||
@@ -385,7 +467,7 @@ class MainActivity : AudioServiceActivity() {
|
||||
) {
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.RECORD_AUDIO),
|
||||
permissionRequestCode
|
||||
visualizerPermissionRequestCode
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -474,7 +556,8 @@ class MainActivity : AudioServiceActivity() {
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode != permissionRequestCode) return
|
||||
if (requestCode == notificationPermissionRequestCode) return
|
||||
if (requestCode != visualizerPermissionRequestCode) return
|
||||
|
||||
if (grantResults.firstOrNull() == PackageManager.PERMISSION_GRANTED) {
|
||||
startVisualizer()
|
||||
|
||||
@@ -18,18 +18,23 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
return
|
||||
}
|
||||
val title = intent.getStringExtra(EXTRA_ALARM_TITLE) ?: "PluriWave"
|
||||
val snoozeMinutes = sanitizeSnoozeMinutes(intent.getIntExtra(EXTRA_SNOOZE_MINUTES, 5))
|
||||
Log.d(TAG, "alarm.receiver action=${intent.action} id=$alarmId title=$title")
|
||||
|
||||
when (intent.action) {
|
||||
ACTION_FIRE -> {
|
||||
AlarmScheduler(context).onAlarmFired(alarmId)
|
||||
PluriWaveAlarmService.start(context, intent)
|
||||
val launch = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_ALARM_ACTION, ACTION_FIRE)
|
||||
putExtra(EXTRA_TRIGGER_AT, intent.getLongExtra(EXTRA_TRIGGER_AT, 0L))
|
||||
putExtra(EXTRA_OCCURRENCE_AT, intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L))
|
||||
putExtra(EXTRA_SNOOZE_MINUTES, snoozeMinutes)
|
||||
}
|
||||
showFireNotification(context, alarmId, title, launch)
|
||||
showFireNotification(context, alarmId, title, launch, snoozeMinutes)
|
||||
try {
|
||||
context.startActivity(launch)
|
||||
Log.d(TAG, "alarm.receiver fire startActivity OK id=$alarmId")
|
||||
@@ -38,12 +43,40 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
ACTION_PRE_NOTICE -> {
|
||||
showPreNoticeNotification(context, alarmId, title)
|
||||
showPreNoticeNotification(
|
||||
context,
|
||||
alarmId,
|
||||
title,
|
||||
snoozeMinutes,
|
||||
intent.getLongExtra(EXTRA_TRIGGER_AT, 0L),
|
||||
intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
|
||||
)
|
||||
}
|
||||
ACTION_POSTPONE_NEXT -> {
|
||||
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
||||
val occurrenceAt = AlarmScheduler(context).postponeNext(alarmId, snoozeMinutes)
|
||||
?: intent.getLongExtra(EXTRA_OCCURRENCE_AT, 0L)
|
||||
val launch = Intent(context, MainActivity::class.java).apply {
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_ALARM_ACTION, ACTION_POSTPONE_NEXT)
|
||||
putExtra(EXTRA_SNOOZE_MINUTES, snoozeMinutes)
|
||||
putExtra(EXTRA_OCCURRENCE_AT, occurrenceAt)
|
||||
putExtra(EXTRA_TRIGGER_AT, intent.getLongExtra(EXTRA_TRIGGER_AT, 0L))
|
||||
}
|
||||
try {
|
||||
context.startActivity(launch)
|
||||
Log.d(TAG, "alarm.receiver postponeNext startActivity OK id=$alarmId")
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "alarm.receiver postponeNext startActivity ERROR id=$alarmId", error)
|
||||
}
|
||||
}
|
||||
ACTION_SKIP_NEXT -> {
|
||||
NotificationManagerCompat.from(context).cancel(notificationIdForAlarm(alarmId))
|
||||
AlarmScheduler(context).skipNext(alarmId)
|
||||
val launch = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_ALARM_ACTION, ACTION_SKIP_NEXT)
|
||||
@@ -63,7 +96,8 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
context: Context,
|
||||
alarmId: String,
|
||||
title: String,
|
||||
launch: Intent
|
||||
launch: Intent,
|
||||
snoozeMinutes: Int
|
||||
) {
|
||||
ensureFireChannel(context)
|
||||
val fullScreenIntent = PendingIntent.getActivity(
|
||||
@@ -82,6 +116,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
.setAutoCancel(false)
|
||||
.setContentIntent(fullScreenIntent)
|
||||
.setFullScreenIntent(fullScreenIntent, true)
|
||||
.addAction(0, "Posponer $snoozeMinutes min", snoozePendingIntent(context, alarmId, snoozeMinutes))
|
||||
.build()
|
||||
|
||||
try {
|
||||
@@ -95,14 +130,21 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPreNoticeNotification(context: Context, alarmId: String, title: String) {
|
||||
private fun showPreNoticeNotification(
|
||||
context: Context,
|
||||
alarmId: String,
|
||||
title: String,
|
||||
snoozeMinutes: Int,
|
||||
triggerAtMillis: Long,
|
||||
occurrenceAtMillis: Long
|
||||
) {
|
||||
ensureChannel(context)
|
||||
|
||||
val openAppIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
requestCode(alarmId, 1),
|
||||
Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_ALARM_ACTION, ACTION_PRE_NOTICE)
|
||||
@@ -121,6 +163,20 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val postponeNextIntent = PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode(alarmId, 3),
|
||||
Intent(context, PluriWaveAlarmReceiver::class.java).apply {
|
||||
action = ACTION_POSTPONE_NEXT
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_ALARM_TITLE, title)
|
||||
putExtra(EXTRA_SNOOZE_MINUTES, snoozeMinutes)
|
||||
putExtra(EXTRA_TRIGGER_AT, triggerAtMillis)
|
||||
putExtra(EXTRA_OCCURRENCE_AT, occurrenceAtMillis)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle(title)
|
||||
@@ -130,7 +186,8 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
.setSilent(true)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(openAppIntent)
|
||||
.addAction(0, "Omitir siguiente", skipNextIntent)
|
||||
.addAction(0, "Posponer $snoozeMinutes min", postponeNextIntent)
|
||||
.addAction(0, "Omitir esta vez", skipNextIntent)
|
||||
.build()
|
||||
|
||||
try {
|
||||
@@ -178,6 +235,21 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
|
||||
private fun requestCode(id: String, slot: Int): Int = 47 * id.hashCode() + slot
|
||||
|
||||
private fun sanitizeSnoozeMinutes(minutes: Int): Int =
|
||||
if (minutes == 3 || minutes == 5 || minutes == 10) minutes else 5
|
||||
|
||||
private fun snoozePendingIntent(context: Context, alarmId: String, minutes: Int): PendingIntent =
|
||||
PendingIntent.getService(
|
||||
context,
|
||||
requestCode(alarmId, 20 + minutes),
|
||||
Intent(context, PluriWaveAlarmService::class.java).apply {
|
||||
action = PluriWaveAlarmService.ACTION_SNOOZE
|
||||
putExtra(EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(PluriWaveAlarmService.EXTRA_SNOOZE_MINUTES, minutes)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val TAG = "PluriWave"
|
||||
const val CHANNEL_ID = "pluriwave_alarm_pre_notice"
|
||||
@@ -185,6 +257,7 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
const val ACTION_FIRE = "es.freetimelab.pluriwave.alarm.FIRE"
|
||||
const val ACTION_PRE_NOTICE = "es.freetimelab.pluriwave.alarm.PRE_NOTICE"
|
||||
const val ACTION_SKIP_NEXT = "es.freetimelab.pluriwave.alarm.SKIP_NEXT"
|
||||
const val ACTION_POSTPONE_NEXT = "es.freetimelab.pluriwave.alarm.POSTPONE_NEXT"
|
||||
const val EXTRA_ALARM_ID = "alarmId"
|
||||
const val EXTRA_ALARM_TITLE = "alarmTitle"
|
||||
const val EXTRA_ALARM_ACTION = "alarmAction"
|
||||
@@ -192,6 +265,9 @@ class PluriWaveAlarmReceiver : BroadcastReceiver() {
|
||||
const val EXTRA_STATION_URL = "stationUrl"
|
||||
const val EXTRA_FALLBACK_SOUND = "fallbackSound"
|
||||
const val EXTRA_VOLUME = "volume"
|
||||
const val EXTRA_TRIGGER_AT = "triggerAtMillis"
|
||||
const val EXTRA_OCCURRENCE_AT = "occurrenceAtMillis"
|
||||
const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
|
||||
|
||||
fun notificationIdForAlarm(alarmId: String): Int = 53 * alarmId.hashCode() + 7
|
||||
fun fireNotificationIdForAlarm(alarmId: String): Int = 59 * alarmId.hashCode() + 9
|
||||
|
||||
@@ -9,7 +9,9 @@ import android.content.Intent
|
||||
import android.media.AudioAttributes
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.PowerManager
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
@@ -21,6 +23,8 @@ class PluriWaveAlarmService : Service() {
|
||||
private var player: MediaPlayer? = null
|
||||
private var wakeLock: PowerManager.WakeLock? = null
|
||||
private var activeAlarmId: String? = null
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private var stationFallbackRunnable: Runnable? = null
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
@@ -34,6 +38,14 @@ class PluriWaveAlarmService : Service() {
|
||||
stopAlarm(requestedId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
ACTION_SNOOZE -> {
|
||||
val minutes = intent.getIntExtra(EXTRA_SNOOZE_MINUTES, 5)
|
||||
if (requestedId != null) {
|
||||
AlarmScheduler(this).snooze(requestedId, minutes)
|
||||
}
|
||||
stopAlarm(requestedId)
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
PluriWaveAlarmReceiver.ACTION_FIRE, null -> startAlarm(intent)
|
||||
else -> Log.w(TAG, "alarm.service unknown action=$action id=$requestedId")
|
||||
}
|
||||
@@ -49,49 +61,164 @@ class PluriWaveAlarmService : Service() {
|
||||
activeAlarmId = alarmId
|
||||
|
||||
val title = intent.getStringExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE) ?: "PluriWave"
|
||||
val stationName = intent.getStringExtra(PluriWaveAlarmReceiver.EXTRA_STATION_NAME)
|
||||
val stationUrl = intent.getStringExtra(PluriWaveAlarmReceiver.EXTRA_STATION_URL)
|
||||
val fallbackSound = intent.getStringExtra(PluriWaveAlarmReceiver.EXTRA_FALLBACK_SOUND)
|
||||
val volume = intent.getFloatExtra(PluriWaveAlarmReceiver.EXTRA_VOLUME, 0.85f).coerceIn(0f, 1f)
|
||||
val snoozeMinutes = sanitizeSnoozeMinutes(
|
||||
intent.getIntExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, 5)
|
||||
)
|
||||
|
||||
acquireWakeLock()
|
||||
startForeground(NOTIFICATION_ID, buildNotification(alarmId, title))
|
||||
startAudio(alarmId, fallbackSound, volume)
|
||||
try {
|
||||
startForeground(NOTIFICATION_ID, buildNotification(alarmId, title, stationName, snoozeMinutes))
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "alarm.service startForeground failed id=$alarmId", error)
|
||||
releaseWakeLock()
|
||||
activeAlarmId = null
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
startAudio(alarmId, stationName, stationUrl, fallbackSound, volume)
|
||||
}
|
||||
|
||||
private fun startAudio(alarmId: String, fallbackSound: String?, volume: Float) {
|
||||
private fun startAudio(
|
||||
alarmId: String,
|
||||
stationName: String?,
|
||||
stationUrl: String?,
|
||||
fallbackSound: String?,
|
||||
volume: Float
|
||||
) {
|
||||
player?.release()
|
||||
player = null
|
||||
|
||||
if (!stationUrl.isNullOrBlank()) {
|
||||
startStationAudio(
|
||||
alarmId,
|
||||
stationName,
|
||||
stationUrl.trim(),
|
||||
fallbackSound,
|
||||
volume
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
startFallbackAudio(alarmId, fallbackSound, volume, "station url missing")
|
||||
}
|
||||
|
||||
private fun startStationAudio(
|
||||
alarmId: String,
|
||||
stationName: String?,
|
||||
stationUrl: String,
|
||||
fallbackSound: String?,
|
||||
volume: Float
|
||||
) {
|
||||
scheduleStationFallback(alarmId, fallbackSound, volume)
|
||||
try {
|
||||
player = MediaPlayer().apply {
|
||||
setAudioAttributes(alarmAudioAttributes())
|
||||
isLooping = false
|
||||
setVolume(volume, volume)
|
||||
setDataSource(stationUrl)
|
||||
setOnPreparedListener {
|
||||
if (activeAlarmId != alarmId) return@setOnPreparedListener
|
||||
cancelStationFallback()
|
||||
it.start()
|
||||
Log.d(
|
||||
TAG,
|
||||
"alarm.service station started id=$alarmId station=$stationName url=$stationUrl"
|
||||
)
|
||||
}
|
||||
setOnCompletionListener {
|
||||
if (activeAlarmId != alarmId) return@setOnCompletionListener
|
||||
Log.w(TAG, "alarm.service station completed id=$alarmId url=$stationUrl")
|
||||
startFallbackAudio(alarmId, fallbackSound, volume, "station completed")
|
||||
}
|
||||
setOnErrorListener { mp, what, extra ->
|
||||
Log.e(
|
||||
TAG,
|
||||
"alarm.service station error id=$alarmId what=$what extra=$extra url=$stationUrl"
|
||||
)
|
||||
runCatching { mp.reset() }
|
||||
if (activeAlarmId == alarmId) {
|
||||
startFallbackAudio(alarmId, fallbackSound, volume, "station error")
|
||||
}
|
||||
true
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
Log.d(TAG, "alarm.service station preparing id=$alarmId station=$stationName url=$stationUrl")
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "alarm.service station prepare failed id=$alarmId url=$stationUrl", error)
|
||||
startFallbackAudio(alarmId, fallbackSound, volume, "station prepare failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFallbackAudio(
|
||||
alarmId: String,
|
||||
fallbackSound: String?,
|
||||
volume: Float,
|
||||
reason: String
|
||||
) {
|
||||
cancelStationFallback()
|
||||
player?.release()
|
||||
player = null
|
||||
|
||||
val source = fallbackAssetPath(fallbackSound)
|
||||
try {
|
||||
player = MediaPlayer().apply {
|
||||
setAudioAttributes(
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
)
|
||||
setAudioAttributes(alarmAudioAttributes())
|
||||
isLooping = true
|
||||
setVolume(volume, volume)
|
||||
setFallbackAssetDataSource(this, fallbackSound)
|
||||
setOnPreparedListener {
|
||||
if (activeAlarmId != alarmId) return@setOnPreparedListener
|
||||
it.start()
|
||||
Log.d(TAG, "alarm.service audio started id=$alarmId source=$source")
|
||||
Log.d(TAG, "alarm.service fallback started id=$alarmId source=$source reason=$reason")
|
||||
}
|
||||
setOnErrorListener { mp, what, extra ->
|
||||
Log.e(TAG, "alarm.service audio error id=$alarmId what=$what extra=$extra source=$source")
|
||||
Log.e(TAG, "alarm.service fallback error id=$alarmId what=$what extra=$extra source=$source")
|
||||
mp.reset()
|
||||
true
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
Log.d(TAG, "alarm.service audio preparing id=$alarmId source=$source")
|
||||
Log.d(TAG, "alarm.service fallback preparing id=$alarmId source=$source reason=$reason")
|
||||
} catch (error: Throwable) {
|
||||
Log.e(TAG, "alarm.service audio prepare failed id=$alarmId source=$source", error)
|
||||
Log.e(TAG, "alarm.service fallback prepare failed id=$alarmId source=$source", error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleStationFallback(
|
||||
alarmId: String,
|
||||
fallbackSound: String?,
|
||||
volume: Float
|
||||
) {
|
||||
cancelStationFallback()
|
||||
val runnable = Runnable {
|
||||
if (activeAlarmId == alarmId) {
|
||||
Log.w(TAG, "alarm.service station timeout id=$alarmId; using fallback")
|
||||
startFallbackAudio(alarmId, fallbackSound, volume, "station timeout")
|
||||
}
|
||||
}
|
||||
stationFallbackRunnable = runnable
|
||||
mainHandler.postDelayed(runnable, STATION_START_TIMEOUT_MILLIS)
|
||||
}
|
||||
|
||||
private fun cancelStationFallback() {
|
||||
stationFallbackRunnable?.let { mainHandler.removeCallbacks(it) }
|
||||
stationFallbackRunnable = null
|
||||
}
|
||||
|
||||
private fun alarmAudioAttributes(): AudioAttributes =
|
||||
AudioAttributes.Builder()
|
||||
.setUsage(AudioAttributes.USAGE_ALARM)
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
|
||||
.build()
|
||||
|
||||
private fun stopAlarm(alarmId: String?) {
|
||||
Log.d(TAG, "alarm.service stop id=$alarmId active=$activeAlarmId")
|
||||
cancelStationFallback()
|
||||
try {
|
||||
player?.stop()
|
||||
} catch (error: Throwable) {
|
||||
@@ -115,29 +242,42 @@ class PluriWaveAlarmService : Service() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun buildNotification(alarmId: String, title: String) =
|
||||
private fun buildNotification(
|
||||
alarmId: String,
|
||||
title: String,
|
||||
stationName: String?,
|
||||
snoozeMinutes: Int
|
||||
) =
|
||||
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_lock_idle_alarm)
|
||||
.setContentTitle("Alarma PluriWave")
|
||||
.setContentText(title)
|
||||
.setContentText(
|
||||
if (stationName.isNullOrBlank()) title else "$title - $stationName"
|
||||
)
|
||||
.setCategory(NotificationCompat.CATEGORY_ALARM)
|
||||
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||
.setOngoing(true)
|
||||
.setAutoCancel(false)
|
||||
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title), true)
|
||||
.setContentIntent(openAlarmPendingIntent(alarmId, title))
|
||||
.setFullScreenIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes), true)
|
||||
.setContentIntent(openAlarmPendingIntent(alarmId, title, snoozeMinutes))
|
||||
.addAction(0, "Posponer $snoozeMinutes min", snoozePendingIntent(alarmId, snoozeMinutes))
|
||||
.addAction(0, "Detener", stopPendingIntent(alarmId))
|
||||
.build()
|
||||
|
||||
private fun openAlarmPendingIntent(alarmId: String, title: String): PendingIntent =
|
||||
private fun openAlarmPendingIntent(
|
||||
alarmId: String,
|
||||
title: String,
|
||||
snoozeMinutes: Int
|
||||
): PendingIntent =
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
requestCode(alarmId, 20),
|
||||
Intent(this, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_TITLE, title)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ACTION, PluriWaveAlarmReceiver.ACTION_FIRE)
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_SNOOZE_MINUTES, snoozeMinutes)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
@@ -153,6 +293,18 @@ class PluriWaveAlarmService : Service() {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun snoozePendingIntent(alarmId: String, minutes: Int): PendingIntent =
|
||||
PendingIntent.getService(
|
||||
this,
|
||||
requestCode(alarmId, 30 + minutes),
|
||||
Intent(this, PluriWaveAlarmService::class.java).apply {
|
||||
action = ACTION_SNOOZE
|
||||
putExtra(PluriWaveAlarmReceiver.EXTRA_ALARM_ID, alarmId)
|
||||
putExtra(EXTRA_SNOOZE_MINUTES, minutes)
|
||||
},
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
private fun acquireWakeLock() {
|
||||
if (wakeLock?.isHeld == true) return
|
||||
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||
@@ -203,6 +355,9 @@ class PluriWaveAlarmService : Service() {
|
||||
return "flutter_assets/assets/audio/$fileName"
|
||||
}
|
||||
|
||||
private fun sanitizeSnoozeMinutes(minutes: Int): Int =
|
||||
if (minutes == 3 || minutes == 5 || minutes == 10) minutes else 5
|
||||
|
||||
override fun onDestroy() {
|
||||
stopAlarm(activeAlarmId)
|
||||
super.onDestroy()
|
||||
@@ -213,6 +368,9 @@ class PluriWaveAlarmService : Service() {
|
||||
private const val CHANNEL_ID = "pluriwave_alarm_native"
|
||||
private const val NOTIFICATION_ID = 92841
|
||||
private const val ACTION_STOP = "es.freetimelab.pluriwave.alarm.STOP_NATIVE"
|
||||
const val ACTION_SNOOZE = "es.freetimelab.pluriwave.alarm.SNOOZE_NATIVE"
|
||||
const val EXTRA_SNOOZE_MINUTES = "snoozeMinutes"
|
||||
private const val STATION_START_TIMEOUT_MILLIS = 15_000L
|
||||
|
||||
fun start(context: Context, source: Intent) {
|
||||
ensureChannel(context)
|
||||
|
||||
@@ -8,7 +8,9 @@ import android.util.Log
|
||||
class PluriWaveBootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_LOCKED_BOOT_COMPLETED,
|
||||
Intent.ACTION_BOOT_COMPLETED,
|
||||
Intent.ACTION_USER_UNLOCKED,
|
||||
Intent.ACTION_MY_PACKAGE_REPLACED,
|
||||
Intent.ACTION_TIME_CHANGED,
|
||||
Intent.ACTION_TIMEZONE_CHANGED,
|
||||
|
||||
Reference in New Issue
Block a user