11、通知与消息同步:NotificationListenerService、通知投屏到车机、消息双向同步、免打扰模式
车机互联里,通知同步这个功能,说实话,是用户感知最强的一个点。
你想想看,开车的时候手机震了一下,是微信消息还是外卖到了?低头看手机?太危险。所以把通知投到车机屏幕上,让用户瞄一眼就知道怎么回事,这才是车机互联该有的体验。
我最早做这个功能的时候,以为不就是读个通知嘛,简单。结果一上手才发现,坑不少。权限、保活、双向同步、还有那个烦人的免打扰逻辑……今天咱们就把这些事掰扯清楚。
11.1 NotificationListenerService 基础
Android 系统里,想监听所有通知,得用 NotificationListenerService。这个服务说白了就是一个系统级的监听器,它能捕获所有应用发出的通知。
注册方式很简单,在 AndroidManifest.xml 里声明:
<service
android:name=".CarNotificationListener"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
注意那个 BIND_NOTIFICATION_LISTENER_SERVICE 权限,这是系统级权限,普通应用拿不到。用户得手动去「设置 -> 无障碍 -> 通知监听」里开启。嗯,这一步经常被忽略,我在项目里就遇到过用户反馈说「怎么收不到通知」,结果一看,权限没开。
核心回调方法就两个:
onNotificationPosted(StatusBarNotification sbn)—— 通知来了onNotificationRemoved(StatusBarNotification sbn)—— 通知被移除
拿到 StatusBarNotification 之后,你可以提取包名、标题、内容、时间戳,甚至还能拿到通知的 PendingIntent,这样点击车机上的通知就能直接跳转到手机 App。
11.2 通知投屏到车机
通知拿到手了,怎么投到车机上?
我习惯的做法是:手机端把通知序列化,通过蓝牙或 Wi-Fi Direct 发送给车机。车机端收到后,解析成自己的 UI 组件展示。
数据格式我用的是 JSON,简单直接:
{
"packageName": "com.tencent.mm",
"appName": "微信",
"title": "张三",
"content": "晚上一起吃饭?",
"postTime": 1698123456789,
"notificationId": 42,
"key": "0|com.tencent.mm|42|null|10123"
}
这里有个坑:notificationId 和 key 必须带上。为什么?因为后续要做消息更新和移除。同一个通知如果内容变了(比如微信消息被撤回),系统会重新发一个 onNotificationPosted,但 key 是一样的。车机端得根据 key 去更新已有的通知,而不是再创建一条新的。
我曾经踩过这个坑——没传 key,结果车机上同一个人的微信消息出现了好几条,用户差点以为手机中毒了。
sbn.getNotification().getSmallIcon() 拿到图标,转成 Base64 字符串,车机端解码显示。不过注意大小,别传太大的图。
11.3 消息双向同步
通知投过去只是第一步。用户可能在车机上操作,比如「标记已读」或者「回复消息」。这就涉及到双向同步了。
双向同步的核心逻辑其实就一句话:车机端的操作,要能回传给手机,手机再调用系统 API 去执行。
举个例子,用户在车机上点了一个微信通知的「已读」按钮:
- 车机发送指令给手机:「标记通知 key=xxx 为已读」
- 手机收到指令,调用
NotificationManager.cancel()移除通知 - 手机再通过
ContentResolver或者直接调微信的接口(如果有)去标记已读
这里有个现实问题:微信、QQ 这些大厂 App,并没有公开的「标记已读」API。怎么办?
我当时的方案是:利用 NotificationListenerService.requestCancelNotification() 方法。这个方法可以移除通知,但不会真的标记已读。不过对于用户来说,通知消失了,心理上就觉得「已处理」了。
如果你对接的是自己的 App,那就好办了。定义一套 AIDL 接口,车机端直接调:
interface INotificationCallback {
void onMarkAsRead(String notificationKey);
void onReply(String notificationKey, String replyText);
void onDismiss(String notificationKey);
}
11.4 免打扰模式
开车的时候,不是所有通知都适合弹出来。比如游戏成就解锁、App 更新提醒这些,投到车机上纯属干扰。
免打扰模式,说白了就是做一层过滤。
我一般分三个层级:
| 层级 | 说明 | 示例 |
|---|---|---|
| 白名单 | 只允许这些 App 的通知投屏 | 微信、电话、短信、地图 |
| 黑名单 | 这些 App 的通知永远不投 | 游戏、工具类、系统更新 |
| 智能过滤 | 根据通知类别或关键字判断 | 只显示「对话」类通知,不显示「推广」类 |
Android 8.0 之后,通知有了「渠道」(Channel)的概念。每个通知都属于一个渠道,渠道有重要性等级。我们可以利用这个来做智能过滤:
// 获取通知渠道
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
int importance = channel.getImportance();
// 只投递 IMPORTANCE_HIGH 及以上的通知
if (importance >= NotificationManager.IMPORTANCE_HIGH) {
// 投屏到车机
} else {
// 忽略
}
另外,车机端也要提供一个「驾驶模式」开关。用户开车时自动开启免打扰,停车后恢复。这个状态变化也要同步给手机,手机端根据状态决定是否继续投递通知。
11.5 整体架构图
下面这张图,是我做这个功能时画的架构图。它把通知从手机到车机的完整链路串起来了:
从图里能看出来,手机端和车机端是对称的。手机负责监听和过滤,车机负责展示和交互。中间两条箭头,一条是通知数据流,一条是操作指令流,互不干扰。
我实际做的时候,还加了一个心跳机制。每隔 30 秒手机和车机互相确认一下连接是否还在。如果断连了,车机端要清掉所有通知,避免显示过时的信息。
好了,通知同步这块的核心内容就这些。从监听、投屏、双向同步到免打扰,每一步都有不少细节。做的时候多想想用户的实际场景,别光顾着技术实现。毕竟,车机互联的最终目的,是让驾驶更安全、更便捷。