Add custom alert rule engine
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package se.ajpanton.notificationsmaster.alerts
|
||||
|
||||
import com.google.re2j.Pattern
|
||||
|
||||
enum class AlertSource {
|
||||
NOTIFICATION_POST,
|
||||
NOTIFICATION_UPDATE,
|
||||
DIRECT_NOTIFICATION_SOUND,
|
||||
DIRECT_NOTIFICATION_VIBRATION,
|
||||
;
|
||||
|
||||
val isDirect get() = this == DIRECT_NOTIFICATION_SOUND || this == DIRECT_NOTIFICATION_VIBRATION
|
||||
}
|
||||
|
||||
enum class AlertOutcome { PLAY_PROFILE, SILENCE_ORIGINAL, PASS_THROUGH }
|
||||
|
||||
enum class AlertTextField { ANY_TEXT, TITLE, BODY, SENDER }
|
||||
|
||||
enum class AlertTextMode { ANY, CONTAINS, REGEX }
|
||||
|
||||
data class AlertProfile(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val soundUri: String? = null,
|
||||
val vibrationPattern: List<Long> = emptyList(),
|
||||
) {
|
||||
init {
|
||||
require(id.isNotBlank() && name.isNotBlank())
|
||||
require(soundUri?.isNotBlank() != false)
|
||||
require(soundUri != null || vibrationPattern.any { it > 0 })
|
||||
require(vibrationPattern.all { it >= 0 })
|
||||
}
|
||||
}
|
||||
|
||||
data class AlertTextMatcher(
|
||||
val field: AlertTextField = AlertTextField.ANY_TEXT,
|
||||
val mode: AlertTextMode = AlertTextMode.ANY,
|
||||
val value: String = "",
|
||||
val caseSensitive: Boolean = false,
|
||||
) {
|
||||
private val pattern = if (mode == AlertTextMode.REGEX) Pattern.compile(
|
||||
value,
|
||||
if (caseSensitive) 0 else Pattern.CASE_INSENSITIVE,
|
||||
) else null
|
||||
|
||||
init {
|
||||
require(mode == AlertTextMode.ANY || value.isNotBlank())
|
||||
}
|
||||
|
||||
fun matches(event: AlertEvent): Boolean = when (mode) {
|
||||
AlertTextMode.ANY -> true
|
||||
AlertTextMode.CONTAINS -> text(event)?.contains(value, ignoreCase = !caseSensitive) == true
|
||||
AlertTextMode.REGEX -> text(event)?.let { pattern!!.matcher(it).find() } == true
|
||||
}
|
||||
|
||||
private fun text(event: AlertEvent): String? = when (field) {
|
||||
AlertTextField.TITLE -> event.title
|
||||
AlertTextField.BODY -> event.body
|
||||
AlertTextField.SENDER -> event.sender
|
||||
AlertTextField.ANY_TEXT -> listOfNotNull(event.title, event.body, event.sender).joinToString("\n").ifBlank { null }
|
||||
}
|
||||
}
|
||||
|
||||
data class AlertRule(
|
||||
val id: String,
|
||||
val packageName: String,
|
||||
val order: Int,
|
||||
val sources: Set<AlertSource>,
|
||||
val matcher: AlertTextMatcher = AlertTextMatcher(),
|
||||
val outcome: AlertOutcome,
|
||||
val profileId: String? = null,
|
||||
val playToCompletion: Boolean = false,
|
||||
val allowDuringDnd: Boolean = false,
|
||||
val enabled: Boolean = true,
|
||||
val name: String? = null,
|
||||
) {
|
||||
init {
|
||||
require(id.isNotBlank() && packageName.isNotBlank() && sources.isNotEmpty())
|
||||
require((outcome == AlertOutcome.PLAY_PROFILE) == (profileId != null))
|
||||
require(name?.isNotBlank() != false)
|
||||
}
|
||||
|
||||
fun groupingKey() = AlertRuleGroupingKey(
|
||||
sources = sources,
|
||||
matcher = matcher,
|
||||
outcome = outcome,
|
||||
profileId = profileId,
|
||||
playToCompletion = playToCompletion,
|
||||
allowDuringDnd = allowDuringDnd,
|
||||
enabled = enabled,
|
||||
)
|
||||
}
|
||||
|
||||
data class AlertRuleGroupingKey(
|
||||
val sources: Set<AlertSource>,
|
||||
val matcher: AlertTextMatcher,
|
||||
val outcome: AlertOutcome,
|
||||
val profileId: String?,
|
||||
val playToCompletion: Boolean,
|
||||
val allowDuringDnd: Boolean,
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
||||
data class AlertAppSettings(
|
||||
val packageName: String,
|
||||
val allowMultipleMatches: Boolean = false,
|
||||
val directAlertControl: Boolean = false,
|
||||
)
|
||||
|
||||
data class AlertEvent(
|
||||
val packageName: String,
|
||||
val source: AlertSource,
|
||||
val title: String? = null,
|
||||
val body: String? = null,
|
||||
val sender: String? = null,
|
||||
)
|
||||
|
||||
data class AlertDecision(
|
||||
val ruleId: String,
|
||||
val outcome: AlertOutcome,
|
||||
val profile: AlertProfile? = null,
|
||||
val playToCompletion: Boolean,
|
||||
val allowDuringDnd: Boolean,
|
||||
)
|
||||
|
||||
data class AlertEvaluation(
|
||||
val decisions: List<AlertDecision>,
|
||||
val silenceUnmatchedDirectAlert: Boolean,
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
package se.ajpanton.notificationsmaster.alerts
|
||||
|
||||
object AlertRuleEvaluator {
|
||||
fun evaluate(
|
||||
app: AlertAppSettings,
|
||||
rules: Iterable<AlertRule>,
|
||||
profiles: Map<String, AlertProfile>,
|
||||
event: AlertEvent,
|
||||
): AlertEvaluation {
|
||||
if (app.packageName != event.packageName || event.source.isDirect && !app.directAlertControl) {
|
||||
return AlertEvaluation(emptyList(), false)
|
||||
}
|
||||
val matches = rules.asSequence()
|
||||
.filter { it.enabled && it.packageName == event.packageName && event.source in it.sources }
|
||||
.sortedWith(compareBy<AlertRule> { it.order }.thenBy { it.id })
|
||||
.filter { it.matcher.matches(event) }
|
||||
.mapNotNull { rule -> rule.decision(profiles[rule.profileId]) }
|
||||
.let { if (app.allowMultipleMatches) it else it.take(1) }
|
||||
.toList()
|
||||
return AlertEvaluation(matches, event.source.isDirect && matches.isEmpty())
|
||||
}
|
||||
|
||||
private fun AlertRule.decision(profile: AlertProfile?): AlertDecision? = when (outcome) {
|
||||
AlertOutcome.PLAY_PROFILE -> profile?.let {
|
||||
AlertDecision(id, outcome, it, playToCompletion, allowDuringDnd)
|
||||
}
|
||||
else -> AlertDecision(id, outcome, null, playToCompletion, allowDuringDnd)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user