Add fail-open LSPosed notification probe

This commit is contained in:
ajp_anton
2026-08-16 03:55:44 +00:00
parent af3673c2e8
commit 9c9ddb6350
7 changed files with 88 additions and 0 deletions
+3
View File
@@ -92,5 +92,8 @@ dependencies {
implementation(libs.swiperefreshlayout) implementation(libs.swiperefreshlayout)
implementation(libs.lifecycle.runtime.ktx) implementation(libs.lifecycle.runtime.ktx)
// Present only when the user enables this APK as an LSPosed module.
compileOnly(libs.libxposed.api)
testImplementation(libs.junit) testImplementation(libs.junit)
} }
@@ -0,0 +1,54 @@
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"
}
}
@@ -0,0 +1,23 @@
package se.ajpanton.notificationsmaster.module
import android.util.Log
import io.github.libxposed.api.XposedModule
import io.github.libxposed.api.XposedModuleInterface
/**
* Optional libxposed entry point. It is inert unless the user enables this APK
* in a compatible framework and grants it a system-server scope.
*/
class NotificationsMasterModule : XposedModule() {
override fun onSystemServerStarting(param: XposedModuleInterface.SystemServerStartingParam) {
runCatching {
NotificationEnqueueProbe(this).install(param.classLoader)
}.onFailure { error ->
Log.e(TAG, "Notification interception probe was not installed; leaving Android unchanged", error)
}
}
private companion object {
const val TAG = "NotificationsMaster"
}
}
@@ -0,0 +1 @@
se.ajpanton.notificationsmaster.module.NotificationsMasterModule
@@ -0,0 +1,3 @@
minApiVersion=101
targetApiVersion=101
staticScope=true
@@ -0,0 +1,2 @@
android
system
+2
View File
@@ -10,6 +10,7 @@ recyclerView = "1.3.2"
swipeRefreshLayout = "1.1.0" swipeRefreshLayout = "1.1.0"
lifecycle = "2.8.7" lifecycle = "2.8.7"
junit = "4.13.2" junit = "4.13.2"
libxposed = "101.0.1"
[libraries] [libraries]
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
@@ -21,6 +22,7 @@ recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version
swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swipeRefreshLayout" } swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swipeRefreshLayout" }
lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "libxposed" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }