diff --git a/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationAttentionBridge.kt b/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationAttentionBridge.kt index c8dfd46..87a5002 100644 --- a/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationAttentionBridge.kt +++ b/app/src/main/java/se/ajpanton/notificationsmaster/module/NotificationAttentionBridge.kt @@ -6,21 +6,13 @@ import android.util.Log import io.github.libxposed.api.XposedInterface import java.lang.reflect.Method -/** - * API 36's standard channel-attention boundary. It deliberately leaves the - * notification itself untouched. - */ +/** Version-specific native-alert boundary with one shared policy decision. */ internal class NotificationAttentionBridge( private val framework: XposedInterface, private val observer: (SystemNotificationEvent) -> Unit, ) { fun install(classLoader: ClassLoader) { - if (Build.VERSION.SDK_INT != 36) return - val helperClass = runCatching { Class.forName(ATTENTION_HELPER, false, classLoader) }.getOrNull() ?: run { - Log.w(TAG, "Notification attention helper was not found; leaving Android unchanged") - return - } - val method = helperClass.declaredMethods.singleOrNull(::isSupportedAttention) ?: run { + val method = attentionMethod(classLoader) ?: run { Log.w(TAG, "Notification attention method shape was not found; leaving Android unchanged") return } @@ -35,9 +27,23 @@ internal class NotificationAttentionBridge( chain.proceed() } } - Log.i(TAG, "Installed API 36 notification attention bridge") + Log.i(TAG, "Installed API " + Build.VERSION.SDK_INT + " notification attention bridge") } + private fun attentionMethod(classLoader: ClassLoader): Method? = when (Build.VERSION.SDK_INT) { + 34 -> findMethod(classLoader, NOTIFICATION_MANAGER_SERVICE, ::isApi34Attention) + 35, 36 -> findMethod(classLoader, ATTENTION_HELPER, ::isApi35PlusAttention) + else -> null + } + + private fun findMethod( + classLoader: ClassLoader, + className: String, + matches: (Method) -> Boolean, + ): Method? = runCatching { + Class.forName(className, false, classLoader).declaredMethods.singleOrNull(matches) + }.getOrNull() + private fun eventFrom(record: Any?): SystemNotificationEvent? = runCatching { val sbn = record?.javaClass?.getMethod("getSbn")?.invoke(record) as? StatusBarNotification ?: return null SystemNotificationEvent.fromStatusBarNotification(sbn) @@ -46,12 +52,20 @@ internal class NotificationAttentionBridge( private companion object { const val TAG = "NotificationsMaster" const val ATTENTION_HELPER = "com.android.server.notification.NotificationAttentionHelper" + const val NOTIFICATION_MANAGER_SERVICE = "com.android.server.notification.NotificationManagerService" + const val NOTIFICATION_RECORD = "com.android.server.notification.NotificationRecord" - fun isSupportedAttention(method: Method): Boolean = + fun isApi34Attention(method: Method): Boolean = + method.name == "buzzBeepBlinkLocked" && + method.returnType == Int::class.javaPrimitiveType && + method.parameterTypes.size == 1 && + method.parameterTypes[0].name == NOTIFICATION_RECORD + + fun isApi35PlusAttention(method: Method): Boolean = method.name == "buzzBeepBlinkLocked" && method.returnType == Int::class.javaPrimitiveType && method.parameterTypes.size == 2 && - method.parameterTypes[0].name == "com.android.server.notification.NotificationRecord" && + method.parameterTypes[0].name == NOTIFICATION_RECORD && method.parameterTypes[1].name.endsWith("NotificationAttentionHelper\$Signals") } } diff --git a/test-helper/src/main/AndroidManifest.xml b/test-helper/src/main/AndroidManifest.xml index 20a787f..7af0472 100644 --- a/test-helper/src/main/AndroidManifest.xml +++ b/test-helper/src/main/AndroidManifest.xml @@ -1 +1,17 @@ - + + + + + + + + + + + + + + + + + diff --git a/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/BackgroundPostReceiver.kt b/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/BackgroundPostReceiver.kt new file mode 100644 index 0000000..d76861f --- /dev/null +++ b/test-helper/src/main/java/se/ajpanton/notificationsmaster/helper/BackgroundPostReceiver.kt @@ -0,0 +1,39 @@ +package se.ajpanton.notificationsmaster.helper + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.media.AudioAttributes +import android.media.RingtoneManager + +/** Posts an alerting test notification without bringing the helper activity forward. */ +class BackgroundPostReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != ACTION) return + val manager = context.getSystemService(NotificationManager::class.java) + manager.createNotificationChannel( + NotificationChannel(CHANNEL, "Test", NotificationManager.IMPORTANCE_HIGH).apply { + setSound( + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION), + AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build(), + ) + enableVibration(true) + vibrationPattern = longArrayOf(0, 200) + }, + ) + manager.notify(ID, Notification.Builder(context, CHANNEL) + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle("Helper") + .setContentText("Text message") + .build()) + } + + companion object { + const val ACTION = "se.ajpanton.notificationsmaster.helper.POST_TEXT" + private const val CHANNEL = "test" + private const val ID = 1 + } +}