Add privileged alert policy endpoint

This commit is contained in:
ajp_anton
2026-08-18 03:22:03 +00:00
parent 77a725ae30
commit 7166cfe18b
5 changed files with 110 additions and 15 deletions
+4
View File
@@ -37,6 +37,10 @@
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".module.AlertPolicyService"
android:exported="true" />
</application>
</manifest>
@@ -11,21 +11,7 @@ object AlertNotificationDispatcher {
event: AlertEvent,
isRoutineUpdate: Boolean,
): List<QueuedAlert> {
if (!configuration.enabled ||
event.source == AlertSource.NOTIFICATION_UPDATE &&
isRoutineUpdate &&
configuration.ignoreRoutineUpdates
) {
return emptyList()
}
val app = configuration.appSettings.firstOrNull { it.packageName == event.packageName }
?: AlertAppSettings(event.packageName)
return AlertRuleEvaluator.evaluate(
app,
configuration.rules,
configuration.profiles.associateBy { it.id },
event,
).decisions.mapNotNull { decision ->
return AlertPolicyEvaluator.evaluate(configuration, event, isRoutineUpdate).decisions.mapNotNull { decision ->
decision.snapshot()?.let { QueuedAlert(decision.ruleId, it) }
}
}
@@ -0,0 +1,21 @@
package se.ajpanton.notificationsmaster.alerts
/** Evaluates saved alert policy without deciding how a caller will play it. */
object AlertPolicyEvaluator {
fun evaluate(
configuration: AlertConfiguration,
event: AlertEvent,
isRoutineUpdate: Boolean,
): AlertEvaluation {
if (!configuration.enabled ||
event.source == AlertSource.NOTIFICATION_UPDATE &&
isRoutineUpdate &&
configuration.ignoreRoutineUpdates
) {
return AlertEvaluation(emptyList(), false)
}
val app = configuration.appSettings.firstOrNull { it.packageName == event.packageName }
?: AlertAppSettings(event.packageName)
return AlertRuleEvaluator.evaluate(app, configuration.rules, configuration.profiles.associateBy { it.id }, event)
}
}
@@ -0,0 +1,64 @@
package se.ajpanton.notificationsmaster.module
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.os.IBinder
import android.os.Parcel
import se.ajpanton.notificationsmaster.alerts.AlertConfigurationStore
import se.ajpanton.notificationsmaster.alerts.AlertEvent
import se.ajpanton.notificationsmaster.alerts.AlertPolicyEvaluator
import se.ajpanton.notificationsmaster.alerts.AlertSource
import se.ajpanton.notificationsmaster.alerts.snapshot
/**
* A deliberately narrow endpoint for the optional system-server module.
* It is exported only so system_server can bind. A bind itself is not a
* sensitive operation; every transaction is authenticated before encrypted
* app data is read.
*/
class AlertPolicyService : Service() {
override fun onBind(intent: Intent): IBinder = PolicyBinder(applicationContext)
private class PolicyBinder(context: android.content.Context) : Binder() {
private val configurationStore = AlertConfigurationStore(context)
init { attachInterface(null, DESCRIPTOR) }
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) return false
if (code == INTERFACE_TRANSACTION) {
reply?.writeString(DESCRIPTOR)
return true
}
if (code != TRANSACTION_EVALUATE || reply == null) return false
data.enforceInterface(DESCRIPTOR)
val packageName = data.readString() ?: return false
val source = runCatching { AlertSource.valueOf(data.readString() ?: return false) }.getOrNull() ?: return false
val event = AlertEvent(packageName, source, data.readString(), data.readString(), data.readString())
val routineUpdate = data.readBoolean()
val result = AlertPolicyEvaluator.evaluate(configurationStore.load(), event, routineUpdate)
reply.writeNoException()
reply.writeBoolean(result.silenceUnmatchedDirectAlert)
reply.writeInt(result.decisions.size)
result.decisions.forEach { decision ->
reply.writeString(decision.ruleId)
reply.writeString(decision.outcome.name)
val snapshot = decision.snapshot()
reply.writeBoolean(snapshot != null)
snapshot?.let {
reply.writeString(it.soundUri)
reply.writeLongArray(it.vibrationPattern.toLongArray())
reply.writeBoolean(it.playToCompletion)
reply.writeBoolean(it.allowDuringDnd)
}
}
return true
}
}
companion object {
const val DESCRIPTOR = "se.ajpanton.notificationsmaster.AlertPolicy"
const val TRANSACTION_EVALUATE = IBinder.FIRST_CALL_TRANSACTION
}
}