Add guarded notification bridge
This commit is contained in:
@@ -1,54 +0,0 @@
|
|||||||
package se.ajpanton.notificationsmaster.module
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.util.Log
|
|
||||||
import io.github.libxposed.api.XposedInterface
|
|
||||||
import java.lang.reflect.Method
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Milestone-one probe for the system-server notification path.
|
|
||||||
*
|
|
||||||
* It deliberately observes only this repository's test helper and always
|
|
||||||
* calls through. No notification is modified, cancelled, or suppressed.
|
|
||||||
*/
|
|
||||||
internal class NotificationEnqueueProbe(private val framework: XposedInterface) {
|
|
||||||
fun install(classLoader: ClassLoader) {
|
|
||||||
val managerClass = Class.forName(NOTIFICATION_MANAGER_SERVICE, false, classLoader)
|
|
||||||
val method = managerClass.declaredMethods.singleOrNull {
|
|
||||||
it.name == ENQUEUE_NOTIFICATION &&
|
|
||||||
it.returnType == Boolean::class.javaPrimitiveType &&
|
|
||||||
it.parameterTypes.any { type -> type.name.endsWith("PostNotificationTracker") }
|
|
||||||
} ?: error("No final $ENQUEUE_NOTIFICATION implementation found")
|
|
||||||
hook(method)
|
|
||||||
Log.i(TAG, "Installed fail-open notification enqueue probe")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hook(method: Method) {
|
|
||||||
method.isAccessible = true
|
|
||||||
framework.hook(method).intercept { chain ->
|
|
||||||
logHelperNotification(chain.args)
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logHelperNotification(args: List<Any?>) {
|
|
||||||
val notification = args.filterIsInstance<Notification>().firstOrNull() ?: return
|
|
||||||
val packageName = args.filterIsInstance<String>().firstOrNull() ?: return
|
|
||||||
if (packageName != TEST_HELPER_PACKAGE) return
|
|
||||||
|
|
||||||
val extras: Bundle = notification.extras ?: Bundle.EMPTY
|
|
||||||
Log.i(
|
|
||||||
TAG,
|
|
||||||
"Observed helper notification before enqueue: title=${extras.getCharSequence(Notification.EXTRA_TITLE)} " +
|
|
||||||
"text=${extras.getCharSequence(Notification.EXTRA_TEXT)}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TAG = "NotificationsMaster"
|
|
||||||
const val NOTIFICATION_MANAGER_SERVICE = "com.android.server.notification.NotificationManagerService"
|
|
||||||
const val ENQUEUE_NOTIFICATION = "enqueueNotificationInternal"
|
|
||||||
const val TEST_HELPER_PACKAGE = "se.ajpanton.notificationsmaster.helper"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-2
@@ -11,13 +11,23 @@ import io.github.libxposed.api.XposedModuleInterface
|
|||||||
class NotificationsMasterModule : XposedModule() {
|
class NotificationsMasterModule : XposedModule() {
|
||||||
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
|
||||||
runCatching {
|
runCatching {
|
||||||
NotificationEnqueueProbe(this).install(param.classLoader)
|
SystemNotificationBridge(this, ::diagnoseHelperEvent).install(param.classLoader)
|
||||||
}.onFailure { error ->
|
}.onFailure { error ->
|
||||||
Log.e(TAG, "Notification interception probe was not installed; leaving Android unchanged", error)
|
Log.e(TAG, "Notification bridge was not installed; leaving Android unchanged", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun diagnoseHelperEvent(event: SystemNotificationEvent) {
|
||||||
|
if (event.packageName != TEST_HELPER_PACKAGE) return
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"Observed helper notification: title=" + event.title + " body=" + event.body?.replace("\n", " | ") +
|
||||||
|
" channel=" + event.channelId + " summary=" + event.isGroupSummary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "NotificationsMaster"
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val TEST_HELPER_PACKAGE = "se.ajpanton.notificationsmaster.helper"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package se.ajpanton.notificationsmaster.module
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import io.github.libxposed.api.XposedInterface
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
import se.ajpanton.notificationsmaster.alerts.AlertEvent
|
||||||
|
import se.ajpanton.notificationsmaster.alerts.AlertSource
|
||||||
|
import se.ajpanton.notificationsmaster.capture.NotificationContents
|
||||||
|
|
||||||
|
/** Rule-relevant data copied from a system-server notification before enqueue. */
|
||||||
|
internal data class SystemNotificationEvent(
|
||||||
|
val packageName: String,
|
||||||
|
val title: String?,
|
||||||
|
val body: String?,
|
||||||
|
val channelId: String?,
|
||||||
|
val isGroupSummary: Boolean,
|
||||||
|
val isOngoing: Boolean,
|
||||||
|
) {
|
||||||
|
fun asAlertEvent(source: AlertSource = AlertSource.NOTIFICATION_POST) =
|
||||||
|
AlertEvent(packageName, source, title, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Version-specific, fail-open system-server boundary.
|
||||||
|
*
|
||||||
|
* It intentionally does not expose [Notification] itself, so later policy code
|
||||||
|
* receives a small immutable snapshot rather than a mutable framework object.
|
||||||
|
*/
|
||||||
|
internal class SystemNotificationBridge(
|
||||||
|
private val framework: XposedInterface,
|
||||||
|
private val observer: (SystemNotificationEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
fun install(classLoader: ClassLoader) {
|
||||||
|
if (Build.VERSION.SDK_INT !in SUPPORTED_SDKS) {
|
||||||
|
Log.i(TAG, "Notification bridge is inactive on API " + Build.VERSION.SDK_INT)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val managerClass = Class.forName(NOTIFICATION_MANAGER_SERVICE, false, classLoader)
|
||||||
|
val method = managerClass.declaredMethods.singleOrNull(::isSupportedEnqueueMethod)
|
||||||
|
if (method == null) {
|
||||||
|
Log.w(TAG, "Notification bridge method shape was not found; leaving Android unchanged")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val notificationIndex = method.parameterTypes.indexOf(Notification::class.java)
|
||||||
|
val packageIndex = method.parameterTypes.indexOfFirst { it == String::class.java }
|
||||||
|
if (notificationIndex < 0 || packageIndex < 0 || packageIndex > notificationIndex) {
|
||||||
|
Log.w(TAG, "Notification bridge method arguments were unexpected; leaving Android unchanged")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
method.isAccessible = true
|
||||||
|
framework.hook(method).intercept { chain ->
|
||||||
|
eventFrom(chain.args, packageIndex, notificationIndex)?.let(observer)
|
||||||
|
chain.proceed()
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Installed API " + Build.VERSION.SDK_INT + " fail-open notification bridge")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun eventFrom(
|
||||||
|
args: List<Any?>,
|
||||||
|
packageIndex: Int,
|
||||||
|
notificationIndex: Int,
|
||||||
|
): SystemNotificationEvent? {
|
||||||
|
val packageName = args.getOrNull(packageIndex) as? String ?: return null
|
||||||
|
val notification = args.getOrNull(notificationIndex) as? Notification ?: return null
|
||||||
|
val title = notification.extras?.getCharSequence(Notification.EXTRA_TITLE)?.toString()
|
||||||
|
return SystemNotificationEvent(
|
||||||
|
packageName = packageName,
|
||||||
|
title = title,
|
||||||
|
body = notificationBody(notification, title),
|
||||||
|
channelId = notification.channelId,
|
||||||
|
isGroupSummary = notification.flags and Notification.FLAG_GROUP_SUMMARY != 0,
|
||||||
|
isOngoing = notification.flags and Notification.FLAG_ONGOING_EVENT != 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notificationBody(notification: Notification, title: String?): String? {
|
||||||
|
val contents = NotificationContents.extract(notification) ?: return null
|
||||||
|
return title?.let { contents.removePrefix(it + "\n") }?.ifBlank { null } ?: contents
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "NotificationsMaster"
|
||||||
|
const val NOTIFICATION_MANAGER_SERVICE = "com.android.server.notification.NotificationManagerService"
|
||||||
|
const val ENQUEUE_NOTIFICATION = "enqueueNotificationInternal"
|
||||||
|
val SUPPORTED_SDKS = 34..36
|
||||||
|
|
||||||
|
fun isSupportedEnqueueMethod(method: Method): Boolean =
|
||||||
|
method.name == ENQUEUE_NOTIFICATION &&
|
||||||
|
method.returnType == Boolean::class.javaPrimitiveType &&
|
||||||
|
method.parameterTypes.any { it.name.endsWith("PostNotificationTracker") } &&
|
||||||
|
method.parameterTypes.count { it == Notification::class.java } == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user