车载通知系统定制:从底层到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_MINPRIORITY_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,中间经过了这么几步:

  1. NMS收到通知,判断是否需要在HUD显示(根据优先级和类型)。
  2. 通过 CarHUDManager 系统服务,把通知内容序列化成HUD协议数据。
  3. HUD硬件(通常是独立的MCU或SoC)解析数据,渲染到风挡玻璃上。

我画了一张流程图,帮你理清这个链路:

NotificationManagerService CarHUDManager (系统服务) HUD HAL (硬件抽象层) 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();
    }
}
避坑指南:HUD的刷新率通常只有30fps,别频繁更新。我曾经让导航每200ms更新一次HUD,结果HUD画面闪烁得像迪斯科。后来改成:转弯提示提前3秒显示,之后每1秒更新一次,就稳了。

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频繁更新,看会不会闪屏。这些坑我都在项目里踩过,提前测好能省很多事。

好了,这一章的内容就到这儿。下一章咱们聊聊车载音频系统的定制——那又是另一个让人头秃的话题了。


公众号:蓝海资料掘金营,微信deep3321