HarmonyOS APP开发中通知监听小知识

举报
Jack20 发表于 2026/06/20 13:51:42 2026/06/20
【摘要】 一、背景想象一下这个场景:你装了10个聊天App,每个App都在发通知,通知栏里乱成一锅粥。如果能有一个"通知管家",把所有聊天通知聚合到一起,按优先级排序,甚至自动过滤掉垃圾通知,那该多好。这就是通知监听的价值所在。通过 NotificationSubscriber,你可以"监听"系统中所有通知的生命周期事件——发布、更新、删除。这让你能够:通知聚合:把分散的通知集中展示,比如一个"消息...

一、背景

想象一下这个场景:你装了10个聊天App,每个App都在发通知,通知栏里乱成一锅粥。如果能有一个"通知管家",把所有聊天通知聚合到一起,按优先级排序,甚至自动过滤掉垃圾通知,那该多好。

这就是通知监听的价值所在。通过 NotificationSubscriber,你可以"监听"系统中所有通知的生命周期事件——发布、更新、删除。这让你能够:

  1. 通知聚合:把分散的通知集中展示,比如一个"消息中心"页面
  2. 智能过滤:自动识别并过滤垃圾通知
  3. 跨设备转发:把手机上的通知转发到平板或手表
  4. 辅助功能:为视障用户提供语音播报通知

不过,通知监听也是一把双刃剑——它能看到所有应用的通知内容,所以权限管控非常严格。HarmonyOS 要求用户必须手动授权,且应用必须声明敏感权限。这就像安装监控摄像头——不是你想装就能装的,需要物业(系统)和住户(用户)的双重同意。


二、核心原理

2.1 通知监听架构

图片.png

2.2 NotificationSubscriber 回调方法

NotificationSubscriber 是一个接口,你需要实现以下回调方法:

回调方法 触发时机 参数
onConsume 新通知发布 SubscribeInfo
onCancel 通知被取消 SubscribeInfo
onUpdate 通知内容更新 SubscribeInfo
onConnect 订阅连接成功
onDisconnect 订阅连接断开
onDestroy 订阅被销毁
onDoNotDisturbDateChange 免打扰模式变更 DoNotDisturbDate
onEnabledNotificationChanged 通知开关变更 EnabledNotificationCallbackData

2.3 SubscribeInfo 数据结构

当通知事件触发时,回调方法会收到 SubscribeInfo 对象,包含通知的完整信息:

interface SubscribeInfo {
  notificationId: number;        // 通知ID
  bundleName: string;            // 来源应用包名
  label: string;                 // 通知标签
  content: NotificationContent;  // 通知内容
  slotType: SlotType;            // 渠道类型
  isUnremovable: boolean;        // 是否不可移除
  isOngoing: boolean;            // 是否常驻
  showDeliveryTime: boolean;     // 是否显示送达时间
  deliveryTime: number;          // 送达时间戳
  tapDismissed: boolean;         // 点击后是否消失
  // ... 更多字段
}

2.4 通知监听安全模型

flowchart TD
    A[应用请求通知监听] --> B{声明权限}
    B -->|未声明| C[编译错误]
    B -->|已声明| D{用户授权}
    D -->|未授权| E[运行时异常]
    D -->|已授权| F[订阅成功]
    
    F --> G[可读取通知内容]
    G --> H{安全约束}
    H --> I[不可修改其他应用通知]
    H --> J[不可拦截其他应用通知]
    H --> K[仅可读取已授权类型]
    
    classDef primary fill:#4CAF50,stroke:#388E3C,color:#fff
    classDef warning fill:#FF9800,stroke:#F57C00,color:#fff
    classDef error fill:#F44336,stroke:#D32F2F,color:#fff
    classDef info fill:#2196F3,stroke:#1976D2,color:#fff
    classDef purple fill:#9C27B0,stroke:#7B1FA2,color:#fff
    
    class A,B primary
    class C,E error
    class D,F,G info
    class H,I,J,K warning

2.5 通知监听与通知发布的区别

特性 通知发布 通知监听
方向 应用→系统 系统→应用
权限 通知权限 通知订阅权限(更严格)
数据访问 自己的通知 所有应用的通知
用途 向用户展示信息 监控和聚合通知
安全级别 普通 敏感

三、代码实战

3.1 基础通知订阅:监听所有通知

这是最基础的通知监听实现——创建 NotificationSubscriber 并订阅系统通知。

import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';

@Entry
@Component
struct NotificationSubscribePage {
  @State subscribeStatus: string = '未订阅';
  @State notificationLog: string = '暂无通知记录';
  @State notificationCount: number = 0;
  private isSubscribed: boolean = false;

  // 保存通知记录
  private notificationRecords: Array<{
    id: number;
    bundleName: string;
    title: string;
    text: string;
    time: string;
  }> = [];

  /**
   * 创建通知订阅者
   * 实现所有回调方法,监听通知生命周期事件
   */
  private createSubscriber(): notificationManager.NotificationSubscriber {
    const subscriber: notificationManager.NotificationSubscriber = {
      // 通知发布时触发 —— 这是最常用的回调
      onConsume: (data: notificationManager.SubscribeInfo) => {
        this.notificationCount++;
        const now = new Date();
        const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;

        // 提取通知内容
        let title = '';
        let text = '';
        if (data.content) {
          if (data.content.normal) {
            title = data.content.normal.title || '';
            text = data.content.normal.text || '';
          }
        }

        // 记录通知信息
        const record = {
          id: data.notificationId,
          bundleName: data.bundleName || '未知应用',
          title,
          text,
          time: timeStr
        };
        this.notificationRecords.unshift(record);  // 最新的在前面

        // 限制记录数量
        if (this.notificationRecords.length > 20) {
          this.notificationRecords.pop();
        }

        // 更新UI
        this.notificationLog = this.formatNotificationLog();
        this.subscribeStatus = `已订阅 | 已接收 ${this.notificationCount} 条通知`;

        console.info(`[通知监听] 新通知: ${title} - 来自 ${data.bundleName}`);
      },

      // 通知取消时触发
      onCancel: (data: notificationManager.SubscribeInfo) => {
        console.info(`[通知监听] 通知取消: ID=${data.notificationId}`);
      },

      // 通知更新时触发
      onUpdate: (data: notificationManager.SubscribeInfo) => {
        console.info(`[通知监听] 通知更新: ID=${data.notificationId}`);
      },

      // 订阅连接成功时触发
      onConnect: () => {
        this.subscribeStatus = '订阅连接成功 ✅';
        this.isSubscribed = true;
        console.info('[通知监听] 订阅连接成功');
      },

      // 订阅断开时触发
      onDisconnect: () => {
        this.subscribeStatus = '订阅已断开';
        this.isSubscribed = false;
        console.info('[通知监听] 订阅断开');
      },

      // 订阅销毁时触发
      onDestroy: () => {
        this.subscribeStatus = '订阅已销毁';
        this.isSubscribed = false;
        console.info('[通知监听] 订阅销毁');
      },

      // 免打扰模式变更时触发
      onDoNotDisturbDateChange: (data: notificationManager.DoNotDisturbDate) => {
        console.info(`[通知监听] 免打扰模式变更: type=${data.type}`);
      },

      // 通知开关变更时触发
      onEnabledNotificationChanged: (data: notificationManager.EnabledNotificationCallbackData) => {
        console.info(`[通知监听] 通知开关变更: bundle=${data.bundle}, enabled=${data.enable}`);
      }
    };

    return subscriber;
  }

  /**
   * 格式化通知日志
   */
  private formatNotificationLog(): string {
    if (this.notificationRecords.length === 0) {
      return '暂无通知记录';
    }

    return this.notificationRecords.map((record, index) => {
      return `${index + 1}. [${record.time}] ${record.bundleName}\n   ${record.title}: ${record.text}`;
    }).join('\n\n');
  }

  /**
   * 开始订阅通知
   */
  async startSubscribe(): Promise<void> {
    if (this.isSubscribed) {
      this.subscribeStatus = '已经在订阅中';
      return;
    }

    try {
      const subscriber = this.createSubscriber();
      await notificationManager.subscribe(subscriber);
      this.subscribeStatus = '订阅请求已发送,等待连接...';
      console.info('[通知监听] 订阅请求已发送');
    } catch (err) {
      const error = err as BusinessError;
      this.subscribeStatus = `订阅失败: ${error.message}`;
      console.error(`[通知监听] 订阅失败: ${error.code}`);
    }
  }

  /**
   * 取消订阅通知
   */
  async stopSubscribe(): Promise<void> {
    if (!this.isSubscribed) {
      this.subscribeStatus = '当前未订阅';
      return;
    }

    try {
      const subscriber = this.createSubscriber();
      await notificationManager.unsubscribe(subscriber);
      this.subscribeStatus = '已取消订阅';
      this.isSubscribed = false;
      console.info('[通知监听] 取消订阅成功');
    } catch (err) {
      const error = err as BusinessError;
      this.subscribeStatus = `取消失败: ${error.message}`;
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('通知监听')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(this.subscribeStatus)
        .fontSize(16)
        .fontWeight(FontWeight.Medium)
        .padding(12)
        .borderRadius(8)
        .backgroundColor(this.isSubscribed ? '#E8F5E9' : '#FFF3E0')
        .width('90%')
        .textAlign(TextAlign.Center)

      Text(`已接收: ${this.notificationCount}`)
        .fontSize(14)
        .fontColor('#666666')

      // 通知日志展示区
      Scroll() {
        Text(this.notificationLog)
          .fontSize(12)
          .width('100%')
          .padding(12)
      }
      .width('90%')
      .height(300)
      .borderRadius(12)
      .backgroundColor('#F0F4F8')

      Row({ space: 16 }) {
        Button('开始订阅')
          .width('40%')
          .height(45)
          .backgroundColor('#4CAF50')
          .onClick(() => this.startSubscribe())

        Button('取消订阅')
          .width('40%')
          .height(45)
          .backgroundColor('#F44336')
          .onClick(() => this.stopSubscribe())
      }

      Button('清空日志')
        .width('80%')
        .height(40)
        .backgroundColor('#757575')
        .onClick(() => {
          this.notificationRecords = [];
          this.notificationLog = '暂无通知记录';
          this.notificationCount = 0;
        })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }
}

3.2 通知过滤与分类:智能通知管家

实际场景中,我们通常不需要监听所有通知,而是只关心特定类型或特定应用的通知。这个示例实现了通知的过滤和分类功能。

import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 通知分类枚举
 */
enum NotificationCategory {
  SOCIAL = '社交通信',
  SERVICE = '服务提醒',
  CONTENT = '内容推荐',
  OTHER = '其他'
}

/**
 * 通知记录数据结构
 */
interface NotificationRecord {
  id: number;
  bundleName: string;
  title: string;
  text: string;
  category: NotificationCategory;
  time: string;
  timestamp: number;
}

@Entry
@Component
struct SmartNotificationFilterPage {
  @State allNotifications: NotificationRecord[] = [];
  @State filteredNotifications: NotificationRecord[] = [];
  @State currentFilter: string = '全部';
  @State statsText: string = '等待订阅...';
  @State isSubscribed: boolean = false;

  // 过滤条件
  private filterOptions: string[] = ['全部', '社交通信', '服务提醒', '内容推荐', '其他'];

  /**
   * 根据渠道类型判断通知分类
   */
  private categorizeNotification(slotType: number): NotificationCategory {
    switch (slotType) {
      case notificationManager.SlotType.SOCIAL_COMMUNICATION:
        return NotificationCategory.SOCIAL;
      case notificationManager.SlotType.SERVICE_INFORMATION:
        return NotificationCategory.SERVICE;
      case notificationManager.SlotType.CONTENT_INFORMATION:
        return NotificationCategory.CONTENT;
      default:
        return NotificationCategory.OTHER;
    }
  }

  /**
   * 创建带过滤功能的通知订阅者
   */
  private createFilteredSubscriber(): notificationManager.NotificationSubscriber {
    return {
      onConsume: (data: notificationManager.SubscribeInfo) => {
        // 提取通知内容
        let title = '';
        let text = '';
        if (data.content?.normal) {
          title = data.content.normal.title || '';
          text = data.content.normal.text || '';
        }

        // 分类通知
        const category = this.categorizeNotification(data.slotType ?? 3);

        // 创建记录
        const now = new Date();
        const record: NotificationRecord = {
          id: data.notificationId,
          bundleName: data.bundleName || '未知',
          title,
          text,
          category,
          time: `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`,
          timestamp: now.getTime()
        };

        // 添加到列表(最多保留50条)
        this.allNotifications.unshift(record);
        if (this.allNotifications.length > 50) {
          this.allNotifications.pop();
        }

        // 应用当前过滤条件
        this.applyFilter(this.currentFilter);

        // 更新统计
        this.updateStats();
      },

      onConnect: () => {
        this.isSubscribed = true;
        this.statsText = '订阅成功,正在监听通知...';
      },

      onDisconnect: () => {
        this.isSubscribed = false;
        this.statsText = '订阅已断开';
      },

      onCancel: () => {},
      onUpdate: () => {},
      onDestroy: () => {
        this.isSubscribed = false;
      },
      onDoNotDisturbDateChange: () => {},
      onEnabledNotificationChanged: () => {}
    };
  }

  /**
   * 应用过滤条件
   */
  private applyFilter(filter: string): void {
    this.currentFilter = filter;
    if (filter === '全部') {
      this.filteredNotifications = [...this.allNotifications];
    } else {
      this.filteredNotifications = this.allNotifications.filter(
        item => item.category === filter
      );
    }
  }

  /**
   * 更新统计信息
   */
  private updateStats(): void {
    const social = this.allNotifications.filter(n => n.category === NotificationCategory.SOCIAL).length;
    const service = this.allNotifications.filter(n => n.category === NotificationCategory.SERVICE).length;
    const content = this.allNotifications.filter(n => n.category === NotificationCategory.CONTENT).length;
    const other = this.allNotifications.filter(n => n.category === NotificationCategory.OTHER).length;

    this.statsText = `总计: ${this.allNotifications.length} | 社交: ${social} | 服务: ${service} | 内容: ${content} | 其他: ${other}`;
  }

  /**
   * 开始订阅
   */
  async startSubscribe(): Promise<void> {
    try {
      const subscriber = this.createFilteredSubscriber();
      await notificationManager.subscribe(subscriber);
    } catch (err) {
      const error = err as BusinessError;
      this.statsText = `订阅失败: ${error.message}`;
    }
  }

  /**
   * 取消订阅
   */
  async stopSubscribe(): Promise<void> {
    try {
      const subscriber = this.createFilteredSubscriber();
      await notificationManager.unsubscribe(subscriber);
      this.isSubscribed = false;
      this.statsText = '已取消订阅';
    } catch (err) {
      const error = err as BusinessError;
      this.statsText = `取消失败: ${error.message}`;
    }
  }

  build() {
    Column({ space: 12 }) {
      Text('智能通知管家')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)

      Text(this.statsText)
        .fontSize(12)
        .fontColor('#666666')
        .width('90%')
        .textAlign(TextAlign.Center)

      // 过滤条件选择
      Row({ space: 8 }) {
        ForEach(this.filterOptions, (option: string) => {
          Button(option)
            .height(32)
            .fontSize(12)
            .backgroundColor(this.currentFilter === option ? '#2196F3' : '#E0E0E0')
            .fontColor(this.currentFilter === option ? '#FFFFFF' : '#333333')
            .onClick(() => this.applyFilter(option))
        })
      }
      .width('90%')
      .justifyContent(FlexAlign.Center)

      // 通知列表
      List({ space: 8 }) {
        ForEach(this.filteredNotifications, (item: NotificationRecord) => {
          ListItem() {
            Row({ space: 12 }) {
              // 分类图标
              Text(item.category === NotificationCategory.SOCIAL ? '💬' :
                   item.category === NotificationCategory.SERVICE ? '📦' :
                   item.category === NotificationCategory.CONTENT ? '📰' : '📌')
                .fontSize(24)

              // 通知内容
              Column({ space: 4 }) {
                Text(item.title)
                  .fontSize(14)
                  .fontWeight(FontWeight.Medium)
                  .maxLines(1)
                Text(`${item.bundleName} · ${item.time}`)
                  .fontSize(11)
                  .fontColor('#999999')
              }
              .alignItems(HorizontalAlign.Start)
              .layoutWeight(1)
            }
            .width('100%')
            .padding(12)
            .borderRadius(8)
            .backgroundColor('#FFFFFF')
          }
        })
      }
      .width('90%')
      .layoutWeight(1)
      .borderRadius(12)
      .backgroundColor('#F5F5F5')
      .padding(8)

      Row({ space: 16 }) {
        Button('开始订阅')
          .width('40%')
          .height(45)
          .backgroundColor('#4CAF50')
          .onClick(() => this.startSubscribe())

        Button('取消订阅')
          .width('40%')
          .height(45)
          .backgroundColor('#F44336')
          .onClick(() => this.stopSubscribe())
      }
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

3.3 通知转发与安全:跨设备通知同步

这个示例展示了如何将监听到的通知转发到其他设备,同时处理安全与隐私问题。

import { notificationManager } from '@kit.NotificationKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 通知转发配置
 */
interface ForwardConfig {
  enabled: boolean;              // 是否启用转发
  filterCategories: string[];    // 允许转发的分类
  stripSensitiveData: boolean;   // 是否脱敏
  maxForwardPerHour: number;     // 每小时最大转发数
}

@Entry
@Component
struct NotificationForwardPage {
  @State logText: string = '通知转发服务就绪';
  @State isSubscribed: boolean = false;
  @State forwardCount: number = 0;
  @State configText: string = '';

  // 转发配置
  private forwardConfig: ForwardConfig = {
    enabled: true,
    filterCategories: ['社交通信', '服务提醒'],  // 只转发社交和服务类
    stripSensitiveData: true,                     // 启用脱敏
    maxForwardPerHour: 30                         // 每小时最多30条
  };

  // 转发计数(用于限流)
  private forwardTimestamps: number[] = [];

  /**
   * 敏感数据脱敏处理
   * 隐藏通知中的手机号、身份证号等敏感信息
   */
  private stripSensitiveInfo(text: string): string {
    if (!this.forwardConfig.stripSensitiveData) {
      return text;
    }

    let result = text;
    // 脱敏手机号:138****1234
    result = result.replace(/1[3-9]\d{9}/g, (match) => {
      return match.substring(0, 3) + '****' + match.substring(7);
    });
    // 脱敏身份证号
    result = result.replace(/\d{17}[\dXx]/g, (match) => {
      return match.substring(0, 6) + '********' + match.substring(14);
    });
    // 脱敏邮箱
    result = result.replace(/[\w.-]+@[\w.-]+\.\w+/g, (match) => {
      const parts = match.split('@');
      return parts[0].substring(0, 2) + '***@' + parts[1];
    });

    return result;
  }

  /**
   * 检查是否超过转发限流
   */
  private checkRateLimit(): boolean {
    const now = Date.now();
    const oneHourAgo = now - 60 * 60 * 1000;

    // 清理1小时前的时间戳
    this.forwardTimestamps = this.forwardTimestamps.filter(ts => ts > oneHourAgo);

    // 检查是否超过限制
    if (this.forwardTimestamps.length >= this.forwardConfig.maxForwardPerHour) {
      return false;  // 超过限流
    }

    // 记录本次转发时间
    this.forwardTimestamps.push(now);
    return true;
  }

  /**
   * 模拟转发通知到其他设备
   * 实际开发中可使用分布式软总线或云端API
   */
  private async forwardNotification(record: {
    bundleName: string;
    title: string;
    text: string;
    category: string;
  }): Promise<void> {
    // 检查转发开关
    if (!this.forwardConfig.enabled) {
      return;
    }

    // 检查分类过滤
    if (!this.forwardConfig.filterCategories.includes(record.category)) {
      return;
    }

    // 检查限流
    if (!this.checkRateLimit()) {
      console.warn('[转发] 超过每小时转发上限');
      return;
    }

    // 脱敏处理
    const safeTitle = this.stripSensitiveInfo(record.title);
    const safeText = this.stripSensitiveInfo(record.text);

    // 模拟转发(实际开发中替换为真实的跨设备通信逻辑)
    console.info(`[转发] 通知已转发: ${safeTitle} - ${safeText}`);
    this.forwardCount++;

    // 在实际项目中,这里可以使用分布式数据管理或RPC进行跨设备通信
    // 例如:
    // await distributedDataObject.setSessionId('notification_sync');
    // await rpc.RemoteObject.sendMessageRequest(...);
  }

  /**
   * 创建带转发功能的通知订阅者
   */
  private createForwardSubscriber(): notificationManager.NotificationSubscriber {
    return {
      onConsume: (data: notificationManager.SubscribeInfo) => {
        let title = '';
        let text = '';
        if (data.content?.normal) {
          title = data.content.normal.title || '';
          text = data.content.normal.text || '';
        }

        // 确定分类
        const categoryMap: Record<number, string> = {
          0: '社交通信',
          1: '服务提醒',
          2: '内容推荐',
          3: '其他'
        };
        const category = categoryMap[data.slotType ?? 3] || '其他';

        // 记录日志
        const now = new Date();
        const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
        this.logText = `[${timeStr}] 收到通知\n` +
          `来源: ${data.bundleName}\n` +
          `分类: ${category}\n` +
          `标题: ${title}\n` +
          `正文: ${text}`;

        // 尝试转发
        this.forwardNotification({
          bundleName: data.bundleName || '',
          title,
          text,
          category
        });
      },

      onConnect: () => {
        this.isSubscribed = true;
        this.logText = '订阅成功,通知转发服务已启动';
      },

      onDisconnect: () => {
        this.isSubscribed = false;
        this.logText = '订阅已断开,转发服务已停止';
      },

      onCancel: () => {},
      onUpdate: () => {},
      onDestroy: () => {
        this.isSubscribed = false;
      },
      onDoNotDisturbDateChange: () => {},
      onEnabledNotificationChanged: () => {}
    };
  }

  /**
   * 开始订阅
   */
  async startSubscribe(): Promise<void> {
    try {
      const subscriber = this.createForwardSubscriber();
      await notificationManager.subscribe(subscriber);
    } catch (err) {
      const error = err as BusinessError;
      this.logText = `订阅失败: ${error.message}`;
    }
  }

  /**
   * 取消订阅
   */
  async stopSubscribe(): Promise<void> {
    try {
      const subscriber = this.createForwardSubscriber();
      await notificationManager.unsubscribe(subscriber);
      this.isSubscribed = false;
      this.logText = '已取消订阅';
    } catch (err) {
      const error = err as BusinessError;
      this.logText = `取消失败: ${error.message}`;
    }
  }

  aboutToAppear(): void {
    // 显示当前配置
    this.configText = `转发: ${this.forwardConfig.enabled ? '开' : '关'} | ` +
      `分类: ${this.forwardConfig.filterCategories.join(', ')} | ` +
      `脱敏: ${this.forwardConfig.stripSensitiveData ? '开' : '关'} | ` +
      `限流: ${this.forwardConfig.maxForwardPerHour}条/小时`;
  }

  build() {
    Scroll() {
      Column({ space: 16 }) {
        Text('通知转发服务')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)

        // 配置信息
        Text(this.configText)
          .fontSize(12)
          .fontColor('#666666')
          .width('90%')
          .padding(8)
          .borderRadius(8)
          .backgroundColor('#E3F2FD')
          .textAlign(TextAlign.Center)

        // 状态信息
        Text(this.logText)
          .fontSize(13)
          .width('90%')
          .padding(16)
          .borderRadius(12)
          .backgroundColor('#F0F4F8')
          .minHeight(100)

        Text(`已转发: ${this.forwardCount} 条通知`)
          .fontSize(16)
          .fontWeight(FontWeight.Medium)

        Row({ space: 16 }) {
          Button('开始订阅')
            .width('40%')
            .height(50)
            .backgroundColor('#4CAF50')
            .onClick(() => this.startSubscribe())

          Button('取消订阅')
            .width('40%')
            .height(50)
            .backgroundColor('#F44336')
            .onClick(() => this.stopSubscribe())
        }

        // 安全提示
        Row() {
          Text('⚠️ 安全提示')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#F44336')
        }
        .width('90%')
        .padding(12)
        .borderRadius(8)
        .backgroundColor('#FFF3E0')

        Text('通知监听涉及用户隐私,请确保:\n' +
          '1. 已获得用户明确授权\n' +
          '2. 敏感数据已脱敏处理\n' +
          '3. 转发数据使用加密传输\n' +
          '4. 遵守当地隐私法规')
          .fontSize(12)
          .fontColor('#666666')
          .width('90%')
      }
      .width('100%')
      .padding(20)
    }
    .width('100%')
    .height('100%')
  }
}

四、踩坑与注意事项

4.1 权限声明与用户授权

通知监听需要两个关键权限,缺一不可:

// module.json5
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.NOTIFICATION_CONTROLLER"  // 通知控制权限
      }
    ]
  }
}

重要ohos.permission.NOTIFICATION_CONTROLLER 是系统签名级别的权限,普通应用无法获取。这意味着通知监听功能主要面向系统应用或与系统签名的应用。

对于普通应用,如果需要实现类似功能,可以考虑:

  • 使用 notificationManager.getActiveNotifications() 获取自己的通知
  • 使用 reminderAgentManager 实现定时提醒
  • 通过应用内消息机制替代系统通知监听

4.2 subscribe 和 unsubscribe 必须使用同一个 subscriber 实例

// ❌ 错误示范:每次创建新的 subscriber
await notificationManager.subscribe(this.createSubscriber());
// 后续取消时又创建了一个新的实例
await notificationManager.unsubscribe(this.createSubscriber());  // 无法取消!

// ✅ 正确做法:保存 subscriber 实例
private subscriber: notificationManager.NotificationSubscriber | null = null;

// 订阅时
this.subscriber = this.createSubscriber();
await notificationManager.subscribe(this.subscriber);

// 取消时
if (this.subscriber) {
  await notificationManager.unsubscribe(this.subscriber);
}

4.3 onConsume 回调中的 UI 更新

onConsume 回调在系统线程中执行,如果需要更新 UI,必须确保状态变量的更新是线程安全的。在 ArkTS 中,@State 变量的更新会自动触发 UI 刷新,但在高频通知场景下,建议做防抖处理:

// ✅ 防抖处理:避免高频通知导致UI卡顿
private pendingUpdate: boolean = false;

onConsume: (data) => {
  // 记录数据
  this.notificationRecords.unshift({...});
  
  // 防抖:100ms内只更新一次UI
  if (!this.pendingUpdate) {
    this.pendingUpdate = true;
    setTimeout(() => {
      this.notificationLog = this.formatNotificationLog();
      this.pendingUpdate = false;
    }, 100);
  }
}

4.4 通知监听与电池消耗

长时间的通知监听会持续消耗电池。建议:

  • 在不需要时及时取消订阅
  • 使用 onDisconnect 回调检测异常断开并重连
  • 避免在后台长时间持有订阅

4.5 隐私合规

通知监听能读取其他应用的通知内容,这涉及严重的隐私问题:

  1. 最小化数据收集:只收集必要的信息,不要存储完整的通知内容
  2. 数据脱敏:转发或存储前必须脱敏处理
  3. 用户知情:在应用中明确告知用户监听的范围和用途
  4. 合规审查:上架前需要通过隐私合规审查

4.6 回调方法的完整性

NotificationSubscriber 接口要求实现所有回调方法。如果某些回调不需要处理,也必须提供空实现:

// ❌ 错误:只实现 onConsume
const subscriber = {
  onConsume: (data) => { ... }
  // 缺少其他回调,编译会报错
};

// ✅ 正确:所有回调都实现
const subscriber = {
  onConsume: (data) => { ... },
  onCancel: () => {},
  onUpdate: () => {},
  onConnect: () => {},
  onDisconnect: () => {},
  onDestroy: () => {},
  onDoNotDisturbDateChange: () => {},
  onEnabledNotificationChanged: () => {}
};

五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5 HarmonyOS 6
订阅方式 同步回调 新增异步回调模式
权限模型 NOTIFICATION_CONTROLLER 新增细粒度监听权限
跨设备监听 不支持 新增分布式通知监听
隐私保护 基础脱敏 新增自动脱敏 API
批量订阅 不支持 新增按应用/渠道批量订阅

5.2 迁移指南

  1. 细粒度权限:HarmonyOS 6 对通知监听权限做了更细粒度的划分,可以指定只监听特定应用的通知:
// HarmonyOS 6 新增:指定监听的应用
const subscribeInfo: notificationManager.SubscribeInfo = {
  bundleNames: ['com.example.chat', 'com.example.mail'],  // 只监听这两个应用
  slotTypes: [notificationManager.SlotType.SOCIAL_COMMUNICATION]  // 只监听社交通信渠道
};
await notificationManager.subscribe(subscriber, subscribeInfo);
  1. 自动脱敏 API:HarmonyOS 6 提供了内置的脱敏工具:
// HarmonyOS 6 新增:自动脱敏
import { privacyUtils } from '@kit.PrivacyKit';

const safeText = privacyUtils.desensitize(originalText, {
  phone: true,     // 脱敏手机号
  email: true,     // 脱敏邮箱
  idCard: true,    // 脱敏身份证
  bankCard: true   // 脱敏银行卡
});
  1. 分布式通知监听:HarmonyOS 6 支持监听同一账号下其他设备的通知,实现跨设备通知同步。

六、总结

mindmap
  root((通知监听))
    核心接口
      NotificationSubscriber
        onConsume 通知消费
        onCancel 通知取消
        onUpdate 通知更新
        onConnect 连接成功
        onDisconnect 断开连接
        onDestroy 销毁
        onDoNotDisturbDateChange 免打扰变更
        onEnabledNotificationChanged 开关变更
    核心API
      subscribe 订阅通知
      unsubscribe 取消订阅
      getActiveNotifications 获取活跃通知
    应用场景
      通知聚合
      智能过滤
      跨设备转发
      辅助功能
    安全与隐私
      NOTIFICATION_CONTROLLER权限
      系统签名级别
      敏感数据脱敏
      转发限流
      隐私合规审查
    注意事项
      subscribe/unsubscribe同一实例
      onConsume中UI防抖
      所有回调必须实现
      及时取消订阅省电
      最小化数据收集
知识点 要点
NotificationSubscriber 通知监听的核心接口,需实现8个回调方法
onConsume 最常用的回调,新通知发布时触发
subscribe/unsubscribe 必须使用同一个 subscriber 实例
权限要求 需要 NOTIFICATION_CONTROLLER 系统签名权限
数据脱敏 转发前必须脱敏手机号、身份证号等敏感信息
限流机制 设置每小时最大转发数,避免通知风暴
UI防抖 高频通知场景下,onConsume 中更新 UI 需做防抖处理
隐私合规 最小化数据收集,用户知情同意,通过合规审查

通知监听是通知体系中最强大也最敏感的能力。它就像一把万能钥匙——能打开所有门,但也意味着巨大的责任。使用时务必遵循最小权限原则,做好安全防护,让这项能力真正为用户服务,而不是成为隐私的漏洞。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。