Android 设备管控开发实战:如何只允许指定 App 或 Channel 的通知出现?

发布时间:2026/8/6 15:11:35
Android 设备管控开发实战:如何只允许指定 App 或 Channel 的通知出现? 受控设备上通常不需要“关闭所有通知”而是只保留业务真正需要的几类管控端自己的前台服务通知、指定业务 App 的全部通知或者某个 App 中用于告警的单一 Channel。其他营销、推荐和无关提醒都不应留在通知栏。这篇只解决这个功能。三方设备使用用户明确开启的通知使用权在通知发布后立即判断并取消自研 ROM 把同一套匹配规则前移到系统通知入队之前。策略下发、版本管理、保活、状态栏和手势控制不在本文展开。本文示例只读取通知的包名与 Channel ID不读取标题、正文、验证码等内容。普通 APK 的通知监听权限可由用户随时关闭因此它不能被描述成不可撤销的系统级管控。1. 先把两种实现位置说清楚两种设备都可以使用同一份白名单区别只在规则执行的位置。设备条件规则执行位置实际效果不能修改系统的三方设备NotificationListenerService.onNotificationPosted()通知已经发布后立即取消少数机型可能短暂闪现可以维护系统的自研 ROMNotificationManagerService入队、展示之前不允许的通知不进入展示链路可以做到无闪现普通应用不能阻止其他 App 调用NotificationManager.notify()也不能修改其他 App 的 Channel。它能做的是在系统回调新通知时调用cancelNotification(key)。因此三方方案的准确表述是“发布后撤下”不是“展示前拦截”。如果业务明确要求通知绝不出现在屏幕上就必须把判断放到自研 ROM 的系统通知链路中。2. 用一个确定的白名单模型完成匹配规则只保留两种模式ALLOW_ALL表示不拦截ALLOW_LIST表示只允许白名单。所谓“全部禁止”就是ALLOW_LIST加空的 App、Channel 白名单不必再维护第三套判断分支。Channel 必须与包名组成二元键。同一个channelId可以被多个 App 使用只按 Channel ID 判断会误放行把二者拼成package_channel字符串又会引入分隔符冲突和解析问题直接使用类型化键更稳妥。enumclassNotificationMode{ALLOW_ALL,ALLOW_LIST}dataclassChannelKey(valpackageName:String,valchannelId:String)dataclassNotificationPolicy(valmode:NotificationMode,valallowedApps:SetStringemptySet(),valallowedChannels:SetChannelKeyemptySet(),valprotectedPackages:SetStringemptySet()){funnormalized():NotificationPolicycopy(allowedAppsallowedApps.map{it.trim()}.filter{it.isNotEmpty()}.toSet(),allowedChannelsallowedChannels.map{ChannelKey(it.packageName.trim(),it.channelId.trim())}.filter{it.packageName.isNotEmpty()it.channelId.isNotEmpty()}.toSet(),protectedPackagesprotectedPackages.map{it.trim()}.filter{it.isNotEmpty()}.toSet())funallows(sbn:StatusBarNotification):Boolean{valpkgsbn.packageNameif(pkginprotectedPackages)returntrueif(modeNotificationMode.ALLOW_ALL)returntrueif(pkginallowedApps)returntrueif(Build.VERSION.SDK_INTBuild.VERSION_CODES.O){valchannelIdsbn.notification.channelIdif(!channelId.isNullOrBlank()ChannelKey(pkg,channelId)inallowedChannels)returntrue}returnfalse}}这段代码同时规定了匹配优先级受保护包优先其次是全部放行再次是 App 白名单然后是(packageName, channelId)白名单最后才取消。App 进入白名单后它的所有 Channel 都被允许App 不在白名单时仍可单独放行其中一个 Channel。protectedPackages至少应包含管控端自身否则“全部禁止”可能把管控端前台服务通知也撤掉。系统通知不要直接整包全部放行而应根据设备用途登记确实不能丢失的系统包电话、紧急告警等能力还要单独做真机验收。Channel 从 Android 8.0API 26开始存在。低于 API 26 时只能按 App 包名控制不能伪造 Channel 级能力。配置 Channel 白名单时以监听回调中的sbn.notification.channelId为准Channel 名称只用于界面展示不能代替稳定 ID。3. 接入并核验通知使用权监听服务需要声明系统绑定权限。该权限写在service上不是让应用自行申请的运行时权限真正的授权动作由设备所有者或管理员在系统“通知使用权”页面完成。服务需要让system_server发现并绑定因此这里设置android:exportedtrueandroid.permission.BIND_NOTIFICATION_LISTENER_SERVICE是系统签名权限负责阻止普通应用绑定该服务。serviceandroid:name.ManagedNotificationListenerandroid:exportedtrueandroid:labelstring/notification_listener_nameandroid:permissionandroid.permission.BIND_NOTIFICATION_LISTENER_SERVICEintent-filteractionandroid:nameandroid.service.notification.NotificationListenerService//intent-filter/service打开授权页后返回应用时必须重新核验而不是把“已经跳转到设置页”当成授权成功。使用 AndroidX 可以避免自行解析Settings.Secure中的扁平字符串。funhasNotificationListenerAccess(context:Context):BooleanNotificationManagerCompat.getEnabledListenerPackages(context).contains(context.packageName)funopenNotificationListenerSettings(context:Context){valaccessPageIntent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)valintentaccessPage.takeIf{it.resolveActivity(context.packageManager)!null}?:Intent(Settings.ACTION_SETTINGS)context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))}Android 13 的POST_NOTIFICATIONS与这里的通知使用权是两件事。前者决定管控端能否展示自己的普通通知后者决定NotificationListenerService能否观察和取消其他 App 的通知。申请了POST_NOTIFICATIONS不会自动获得监听能力用户关闭通知使用权后白名单过滤也会立即失效。4. 三方设备监听、匹配、取消与热更新通知回调必须走一条很短的路径读取当前不可变快照、匹配、必要时按通知key取消。不要在每条通知到来时读取数据库、解析远程策略或执行网络请求。4.1 用原子快照替换整份规则AtomicReference让回调始终读到一份完整规则不会看到“App 白名单已经更新、Channel 白名单还没更新”的中间状态。下面的观察者只负责通知服务清理存量通知持久化仍由现有本地存储完成并应在进程启动时同步恢复最后一份已验证策略。objectNotificationPolicyCenter{privatevalcurrentAtomicReference(NotificationPolicy(NotificationMode.ALLOW_ALL))privatevalobserversCopyOnWriteArraySet()-Unit()funsnapshot():NotificationPolicycurrent.get()funreplace(next:NotificationPolicy){current.set(next.normalized())observers.forEach{it.invoke()}}funaddObserver(observer:()-Unit){observersobserver}funremoveObserver(observer:()-Unit){observers-observer}}示例默认ALLOW_ALL目的是避免本地策略尚未恢复时误删系统通知。量产应用应在通知服务开始处理回调前同步加载持久化快照若业务要求启动阶段“默认拒绝”也必须先把管控端自身和关键系统包加入受保护名单。4.2 服务只做即时判断和存量清理classManagedNotificationListener:NotificationListenerService(){privatevalmainHandlerHandler(Looper.getMainLooper())VolatileprivatevarconnectedfalseprivatevalonPolicyChanged:()-Unit{mainHandler.post{sweepBlockedNotifications()}}overridefunonCreate(){super.onCreate()NotificationPolicyCenter.addObserver(onPolicyChanged)}overridefunonNotificationPosted(sbn:StatusBarNotification){valpolicyNotificationPolicyCenter.snapshot()if(!policy.allows(sbn)){cancelNotification(sbn.key)}}overridefunonListenerConnected(){super.onListenerConnected()connectedtruesweepBlockedNotifications()}overridefunonListenerDisconnected(){connectedfalsesuper.onListenerDisconnected()if(Build.VERSION.SDK_INTBuild.VERSION_CODES.N){requestRebind(ComponentName(this,javaClass))}}privatefunsweepBlockedNotifications(){if(!connected)returnvalpolicyNotificationPolicyCenter.snapshot()valvisiblerunCatching{activeNotifications}.getOrNull().orEmpty()visible.forEach{sbn-if(!policy.allows(sbn)){cancelNotification(sbn.key)}}}overridefunonDestroy(){NotificationPolicyCenter.removeObserver(onPolicyChanged)mainHandler.removeCallbacksAndMessages(null)connectedfalsesuper.onDestroy()}}这里有三个容易漏掉的实现点。策略收紧必须清理存量通知。只在onNotificationPosted()中判断只能处理之后新发出的通知。白名单从 App 级收紧到单一 Channel 后之前已经显示的其他 Channel 通知仍会留在通知栏所以replace()后必须扫描activeNotifications。requestRebind()只用于断开后的恢复。onListenerConnected()已经说明绑定成功在该回调里再次请求绑定没有意义。重新连接后先清理一次存量通知避免断开期间累积的通知继续显示。不需要调用super.onNotificationPosted()来“放行”。该回调只是系统通知监听入口不调用cancelNotification()就是保留。匹配逻辑也不应读取通知正文这样既降低隐私风险也避免验证码、消息内容等敏感信息进入日志。5. 自研 ROM把同一规则前移到展示之前自研系统仍使用前面的 App/Channel 匹配器但执行位置放在frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java。在当前 Android 分支完成NotificationRecord和 Channel 解析后、提交入队或调度展示之前增加管控门禁不要等通知已经交给监听器再取消。// 伪代码具体函数名随 Android 分支变化插入位置语义不变。NotificationRecordrecordbuildNotificationRecord(...);StringpackageNamerecord.getSbn().getPackageName();NotificationChannelchannelrecord.getChannel();StringchannelIdchannelnull?null:channel.getId();if(!mManagedNotificationGate.allows(packageName,channelId)){logManagedNotificationDrop(packageName,channelId);return;// 不进入 enqueue / ranking / display 链路}enqueueNotificationRecord(record);规则更新接口应由system_server暴露并使用签名级权限保护。管控端只调用“恢复全部通知”“替换 App 白名单”“替换 AppChannel 白名单”这类最小 API真正的最终判断仍在系统服务中完成不能让普通应用通过广播或可伪造参数绕过。Android 大版本升级时首先回归的是这个拦截点是否仍位于 Channel 解析之后、通知入队之前。ROM 路线与三方路线不是谁替代谁能维护系统时选择展示前门禁不能修改系统时选择通知监听和发布后撤下。两者共享规则模型验收标准则必须按实际控制位置分别制定。6. 用结果矩阵验收不只看回调日志操作预期结果模式为ALLOW_ALL此后新通知和当前仍可见的通知保留已经取消的通知不会自动恢复App A 在 App 白名单App A 的所有 Channel 都保留App B 不在 App 白名单但(B, alarm)在 Channel 白名单只保留 App B 的alarmChannelApp C 也使用alarmChannel仍被取消不能因同名 Channel 误放行从 App A 白名单收紧为(A, alarm)App A 已显示的其他 Channel 通知立即被清理管控端正在运行前台服务管控端自身通知始终保留通知监听连接断开后恢复重连后重新扫描并清理不允许的存量通知用户关闭通知使用权三方方案停止过滤界面必须明确显示“未授权”Android 7.1 及以下只验收 App 级白名单不声称支持 Channel三方设备还要增加一项肉眼测试连续发送高优先级或悬浮通知观察是否发生短暂闪现。NotificationListenerService收到的是“已发布”回调调度和厂商实现决定了取消前是否来得及显示代码不能把这个物理边界包装成零闪现。系统关键通知还可能无法取消、被系统重新发布或不经过普通第三方通知链路因此必须在目标 ROM 和目标业务场景中逐项验证。到这里功能闭环只有四步恢复一份完整策略快照收到通知后按固定优先级匹配策略变化时清理存量通知断线重连后再次收敛。三方设备做到发布后撤下自研 ROM 做到展示前阻断这就是“只允许指定 App 或 Channel 的通知出现”应当明确交付的边界。参考资料Android DevelopersNotificationListenerServiceAndroid DevelopersStatusBarNotificationAndroid DevelopersCreate and manage notification channelsAndroid DevelopersNotification runtime permissionAOSPNotificationManagerService本文根据真实设备管控项目中的通知模式、App 白名单、Channel 白名单、通知监听取消链路和自研 ROM 接口重新抽象并脱敏类型化 Channel 键、不可变策略快照、存量通知清理与连接恢复是面向本文目标重新设计的清洁实现。