车载通知系统定制:从底层到HUD的全链路改造
通知系统,说白了就是车机和用户沟通的「传话筒」。但车载场景和手机完全不同——你想想看,驾驶员在高速上哪有空划掉一堆推送?我最早参与车载项目时,产品经理扔过来一句话:「把手机那套通知搬过来就行。」结果呢?测试车跑了一圈,驾驶员差点被弹窗吓到打方向盘。嗯,从那以后我就明白,车载通知必须重新设计。
今天咱们就聊聊,怎么从 NotificationManagerService 底层开始,把通知系统改造成真正适合车载的样子。我会结合自己踩过的坑,把优先级、HUD显示、历史去重这些关键点掰开揉碎。
1. NotificationManagerService 定制:从AOSP到车载
Android原生的 NotificationManagerService(简称NMS)是为手机设计的。它假设用户随时能看屏幕、能交互。但车载场景下,很多通知根本不该出现,或者出现方式要变。
我习惯的做法是:不直接改AOSP源码,而是通过系统服务扩展点来定制。这样后续升级Android版本时,冲突会少很多。
1.1 核心改造点
| 模块 | 手机默认行为 | 车载定制行为 |
|---|---|---|
| 通知过滤 | 所有应用均可发送 | 白名单机制,仅系统应用和认证应用 |
| 通知排序 | 按时间倒序 | 按优先级+时间混合排序 |
| 通知展示 | 状态栏+锁屏 | 状态栏+HUD+语音播报 |
| 通知交互 | 点击跳转应用 | 点击触发车载动作(如导航、电话) |
我在项目中遇到过最头疼的问题:第三方地图App疯狂推送广告通知。后来我们在NMS的 enqueueNotificationInternal 方法里加了一层过滤,只有签名证书在白名单里的应用才能发通知。代码大概长这样:
// 在 NotificationManagerService.java 中
@Override
public void enqueueNotificationInternal(String pkg, int uid, int pid,
String tag, int id, Notification notification, int userId) {
// 车载定制:白名单检查
if (!isCarNotificationAllowed(pkg, uid)) {
Slog.w(TAG, "Blocked notification from " + pkg);
return;
}
// 原有逻辑继续...
super.enqueueNotificationInternal(pkg, uid, pid, tag, id, notification, userId);
}
private boolean isCarNotificationAllowed(String pkg, int uid) {
// 检查系统签名、预置应用、或动态授权
return mCarNotificationPolicy.isPackageAllowed(pkg, uid);
}
/system/etc/car_notification_policy.xml 配置文件,OTA升级时可以动态更新。我曾经因为写死白名单,导致某次系统更新后第三方导航无法推送交通事件,被用户骂惨了。
2. 车载通知优先级:别让驾驶员分心
手机上的通知优先级(PRIORITY_MIN 到 PRIORITY_MAX)在车里根本不够用。你想想看,「低电量提醒」和「前方事故预警」能是一个级别吗?
我重新定义了车载通知优先级体系,分为5级:
- CRITICAL(紧急):安全相关,如碰撞预警、刹车故障。必须立即显示+HUD+语音。
- HIGH(高):驾驶相关,如导航转弯、来电。显示+HUD,可语音。
- NORMAL(普通):系统提示,如油量低、保养提醒。仅显示,不打断。
- LOW(低):资讯类,如天气、新闻。折叠显示,不主动弹出。
- MIN(最低):后台同步、日志等。不显示,仅记录历史。
实现上,我在 NotificationRecord 里加了一个 carPriority 字段,排序时优先按这个字段排:
// 自定义排序比较器
private static final Comparator<NotificationRecord> CAR_PRIORITY_COMPARATOR = (a, b) -> {
int carPriA = a.getCarPriority();
int carPriB = b.getCarPriority();
if (carPriA != carPriB) {
return carPriB - carPriA; // 高优先级在前
}
// 同优先级按时间倒序
return Long.compare(b.getRankingTimeMs(), a.getRankingTimeMs());
};
carPriority。我见过某音乐App把自己的通知设成CRITICAL,结果每次切歌都弹HUD,驾驶员差点投诉到车厂。这个字段必须由系统服务根据通知类型和来源自动赋值。
3. HUD通知显示:把信息投射到风挡上
HUD(抬头显示)是车载通知的「杀手级」功能。但HUD空间有限,不能像中控屏那样展示大段文字。我总结了一条铁律:HUD上只放3秒内能读完的信息。
3.1 HUD通知的渲染流程
从NMS到HUD,中间经过了这么几步:
- NMS收到通知,判断是否需要在HUD显示(根据优先级和类型)。
- 通过
CarHUDManager系统服务,把通知内容序列化成HUD协议数据。 - HUD硬件(通常是独立的MCU或SoC)解析数据,渲染到风挡玻璃上。
我画了一张流程图,帮你理清这个链路:
3.2 HUD通知的内容裁剪
HUD上显示什么?我定了个规则:
- 标题:最多8个汉字(或12个英文字符)。
- 内容:最多20个汉字(或30个英文字符)。
- 图标:必须用系统预置的HUD图标集,不能用应用自定义图标。
- 持续时间:CRITICAL通知显示5秒,HIGH显示3秒,其他不显示。
代码里,我在 CarHUDManager 中做了内容截断:
public class CarHUDManager {
private static final int MAX_TITLE_LEN = 8; // 汉字
private static final int MAX_CONTENT_LEN = 20;
public HUDNotification buildHUDNotification(NotificationRecord record) {
String title = truncateText(record.getTitle(), MAX_TITLE_LEN);
String content = truncateText(record.getContentText(), MAX_CONTENT_LEN);
int iconId = mapToHUDIcon(record.getSmallIcon());
int duration = getHUDDuration(record.getCarPriority());
return new HUDNotification(title, content, iconId, duration);
}
private String truncateText(String text, int maxLen) {
if (text == null) return "";
// 中文字符算1个,英文字符算0.5个
int len = 0;
StringBuilder sb = new StringBuilder();
for (char c : text.toCharArray()) {
len += (c >= 0x4E00 && c <= 0x9FFF) ? 1 : 0.5;
if (len > maxLen) break;
sb.append(c);
}
return sb.toString();
}
}
4. 通知历史与去重:别让用户看到「已读」消息
车载场景下,通知历史是个双刃剑。一方面,驾驶员错过通知后需要回溯;另一方面,历史列表里堆满重复通知会让人抓狂。
4.1 通知历史存储
我不用Android原生的 NotificationHistory,因为它的存储格式太「手机化」了。我自定义了一个轻量级SQLite数据库,只存最近100条通知:
CREATE TABLE car_notification_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pkg TEXT NOT NULL,
tag TEXT,
notification_id INTEGER,
title TEXT,
content TEXT,
car_priority INTEGER,
timestamp INTEGER,
is_read INTEGER DEFAULT 0,
source TEXT DEFAULT 'system' -- 'system', 'hud', 'voice'
);
查询时,按 car_priority 降序、timestamp 降序排列。这样紧急通知永远在最前面。
4.2 去重策略
去重是车载通知最容易被忽视的点。我遇到过最离谱的情况:某款音乐App每次网络切换都发一条「网络已连接」通知,5分钟发了30条。用户直接在论坛开骂。
我的去重逻辑分三层:
- 第一层:相同pkg+tag+id。这是Android原生的去重,直接替换旧通知。
- 第二层:相同pkg+相似内容。用SimHash算法计算内容相似度,超过0.8就认为是重复。
- 第三层:相同pkg+短时间窗口。5秒内同一应用发送的多条通知,只保留最后一条。
代码实现大概这样:
// 在 CarNotificationManager 中
public boolean isDuplicate(NotificationRecord newRecord) {
String pkg = newRecord.getPackageName();
long now = System.currentTimeMillis();
// 第一层:精确匹配
for (NotificationRecord old : mRecentRecords) {
if (old.getPackageName().equals(pkg) &&
Objects.equals(old.getTag(), newRecord.getTag()) &&
old.getId() == newRecord.getId()) {
return true;
}
}
// 第二层:相似内容
String newContent = newRecord.getContentText();
for (NotificationRecord old : mRecentRecords) {
if (old.getPackageName().equals(pkg)) {
double similarity = computeSimHashSimilarity(
old.getContentText(), newContent);
if (similarity > 0.8) {
return true;
}
}
}
// 第三层:时间窗口
for (NotificationRecord old : mRecentRecords) {
if (old.getPackageName().equals(pkg) &&
(now - old.getTimestamp()) < 5000) {
return true;
}
}
return false;
}
carPriority == CRITICAL 的通知跳过第二层和第三层去重。
5. 总结一下
车载通知系统定制,说白了就是三个字:控、简、准。
- 控:控制通知来源,别让乱七八糟的应用骚扰驾驶员。
- 简:简化显示内容,HUD上只放最核心的信息。
- 准:准确去重,别让用户看到一堆「已读」垃圾。
我个人习惯在项目初期就搭好通知系统的自动化测试用例。比如模拟100条重复通知,看系统会不会崩溃;模拟HUD频繁更新,看会不会闪屏。这些坑我都在项目里踩过,提前测好能省很多事。
好了,这一章的内容就到这儿。下一章咱们聊聊车载音频系统的定制——那又是另一个让人头秃的话题了。