Add privileged alert policy endpoint
This commit is contained in:
@@ -37,6 +37,10 @@
|
|||||||
<action android:name="android.service.notification.NotificationListenerService" />
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".module.AlertPolicyService"
|
||||||
|
android:exported="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
+1
-15
@@ -11,21 +11,7 @@ object AlertNotificationDispatcher {
|
|||||||
event: AlertEvent,
|
event: AlertEvent,
|
||||||
isRoutineUpdate: Boolean,
|
isRoutineUpdate: Boolean,
|
||||||
): List<QueuedAlert> {
|
): List<QueuedAlert> {
|
||||||
if (!configuration.enabled ||
|
return AlertPolicyEvaluator.evaluate(configuration, event, isRoutineUpdate).decisions.mapNotNull { decision ->
|
||||||
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 ->
|
|
||||||
decision.snapshot()?.let { QueuedAlert(decision.ruleId, it) }
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.alerts
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AlertPolicyEvaluatorTest {
|
||||||
|
private val profile = AlertProfile("profile", "Profile", vibrationPattern = listOf(0, 10))
|
||||||
|
private val rule = AlertRule("rule", "chat.app", 0, setOf(AlertSource.NOTIFICATION_POST), outcome = AlertOutcome.PLAY_PROFILE, profileId = profile.id)
|
||||||
|
|
||||||
|
@Test fun `returns no decision when alerting is disabled`() {
|
||||||
|
assertTrue(AlertPolicyEvaluator.evaluate(AlertConfiguration(enabled = false, profiles = listOf(profile), rules = listOf(rule)), event(), false).decisions.isEmpty())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun `returns the evaluated decision for a post`() {
|
||||||
|
assertEquals(listOf("rule"), AlertPolicyEvaluator.evaluate(AlertConfiguration(profiles = listOf(profile), rules = listOf(rule)), event(), false).decisions.map { it.ruleId })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun event() = AlertEvent("chat.app", AlertSource.NOTIFICATION_POST)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user