如何为不同优先级的通知设置不同的声音、震动和图标?

解读

面试官问的不是“能不能”,而是“怎么做、做得稳、做得合规”。国内 ROM 对通知管控极严,Target SDK 34 以后前台通知必须配渠道,后台通知还要受“省电→无限制”与“通知管理→类别”双重钳制。因此答案必须同时覆盖:

  1. AOSP 原生机制(渠道、重要性)
  2. 国内厂商兼容(小米、华为、OPPO、vivo、荣耀、三星)的“私有字段 + 系统权限”差异
  3. 运行时用户可关闭的风险与降级策略
  4. 8.0 以下旧机型的 Fallback

知识点

  • NotificationChannel:8.0+ 唯一入口,重要性级别 1-4 对应静默、低、默认、高
  • 渠道属性:setSound(Uri, AudioAttributes)、setVibrationPattern(long[])、setBypassDnd(true) 需 Manifest 声明 ACCESS_NOTIFICATION_POLICY
  • 国内厂商: – 小米:EXTRA_MESSAGE_TYPE、Notification.Builder.setChannelId 后仍要 setSound/CustomView 二次设置,且 MIUI 12+ 强制“悬浮通知”开关 – 华为:HMS 渠道 HwNotificationManager,setImportance 与原生枚举值错位 +1 – OPPO:ColorOS 7 以后禁止自定义震动,只能使用系统内置模式 – vivo:Funtouch 10 以下忽略 setSound,需跳转到“i 管家”引导用户手动开
  • 图标:5.0+ 强制单色矢量,透明通道决定着色;国内部分 ROM 会取应用桌面图标缓存,需同时提供 res/mipmap 与 res/drawable-anydpi-v24
  • 权限:Android 13 新增 POST_NOTIFICATIONS;小米、华为额外需要“后台弹出界面”权限
  • 版本兼容:Support Library 26 以上 NotificationCompat.Builder 内部自动代理渠道;7.x 以下使用 deprecated setPriority 与 setSound/setVibration
  • 测试命令:adb shell dumpsys notification 查看 rank、importance、sound|vibrate 字段

答案

  1. 定义渠道枚举
enum class NotifyPriority {
    URGENT,  // 高优先级,响铃+长震+悬浮
    NORMAL,  // 默认优先级,短震+提示音
    SILENT   // 低优先级,静默
}
  1. 创建并提交渠道(仅首次)
private fun createChannels(ctx: Context) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
    val nm = ctx.getSystemService(NotificationManager::class.java)
    listOf(
        NotificationChannel(
            "urgent", "紧急消息",
            NotificationManager.IMPORTANCE_HIGH
        ).apply {
            setSound(
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION),
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build()
            )
            vibrationPattern = longArrayOf(0, 300, 200, 300)
            enableLights(true)
            lightColor = Color.RED
            setBypassDnd(true)   // 需 Manifest 声明 ACCESS_NOTIFICATION_POLICY
        },
        NotificationChannel(
            "normal", "普通消息",
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            vibrationPattern = longArrayOf(0, 200)
            setSound(
                Uri.parse("android.resource://${ctx.packageName}/raw/notify_normal"),
                AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                    .build()
            )
        },
        NotificationChannel("silent", "静默通知",
            NotificationManager.IMPORTANCE_LOW)
    ).forEach { nm.createNotificationChannel(it) }
}
  1. 国内厂商补丁(以小米为例)
fun buildForMiui(ctx: Context, priority: NotifyPriority): Notification {
    val builder = NotificationCompat.Builder(ctx, priority.name.lowercase())
        .setSmallIcon(R.drawable.ic_notify_mono)
        .setContentTitle("标题")
        .setContentText("内容")
    when (priority) {
        NotifyPriority.URGENT -> {
            builder.setDefaults(NotificationCompat.DEFAULT_LIGHTS or NotificationCompat.DEFAULT_SOUND)
                   .setVibrate(longArrayOf(0, 300, 200, 300))
            // MIUI 12+ 需要额外申请“悬浮通知”权限
            if (MiuiUtils.isMiui() && !MiuiUtils.canFloat(ctx)) {
                MiuiUtils.reqFloat(ctx) // 跳转到权限页
            }
        }
        NotifyPriority.NORMAL -> {
            builder.setSound(Uri.parse("android.resource://${ctx.packageName}/raw/notify_normal"))
                   .setVibrate(longArrayOf(0, 200))
        }
        else -> { /* silent */ }
    }
    return builder.build()
}
  1. 图标适配
res/
  drawable-anydpi-v24/
     ic_notify_mono.xml  // 单色矢量,透明通道
  mipmap-xxxhdpi/
     ic_launcher.png     // 桌面图标,防止部分 ROM 回退取错图
  1. 权限声明
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
<!-- 国内厂商额外权限 -->
<uses-permission android:name="com.huawei.permission.external_app_settings.USE_COMPONENT"/>
<uses-permission android:name="oppo.permission.OPPO_COMPONENT_SAFE"/>
  1. 7.x 及以下 Fallback
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    val priority = when(priority){
        NotifyPriority.URGENT -> NotificationCompat.PRIORITY_HIGH
        NotifyPriority.NORMAL -> NotificationCompat.PRIORITY_DEFAULT
        else -> NotificationCompat.PRIORITY_LOW
    }
    builder.setPriority(priority)
           .setSound(uri)
           .setVibrate(pattern)
}
  1. 发送
with(NotificationManagerCompat.from(ctx)) {
    if (ActivityCompat.checkSelfPermission(ctx, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
        notify(tag, id, notification)
    }
}

拓展思考

  1. 用户关闭渠道后如何降级?监听 NotificationManagerCompat.getNotificationChannel(channelId).importance == IMPORTANCE_NONE,转用 In-App 横幅或短信补位
  2. 折叠屏/车载双屏场景:8.0+ 允许为同一渠道再分 Bubble、Conversation,通过 setShortcutId + Person 实现“重要对话”独立声音
  3. 隐私沙盒与 FCM:未来无设备标识条件下,高优通知需依赖 FCM priority=HIGH 触发 App Standby Bucket 升档,否则渠道再高也可能被限
  4. 测试自动化:利用 adb shell cmd notification post 带 --es sound/--es vibration 参数模拟不同优先级,结合 uiautomator 检查悬浮、锁屏、横幅是否按预期出现