DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

Android 13 通知不显示也不弹权限框:检查 POST_NOTIFICATIONS 和通知渠道

Android 13 通知不显示时,区分 targetSdk 对弹窗的影响,检查 POST_NOTIFICATIONS、用户选择、升级预授权及通知渠道。

应用在 Android 13 上调用了 notify(),没有报错,通知栏却没有消息;创建了通知渠道,权限框也没有出现。先记录设备 API、应用 targetSdk,以及这次是全新安装、覆盖更新,还是从旧手机恢复。通知权限的初始状态和弹窗时机与这些条件有关。

本文针对普通应用通知。通知能否显示至少经过应用级通知开关、Android 13 的 POST_NOTIFICATIONS 权限,以及通知渠道设置。推送消息是否已到达应用是更前面的一层,应先用应用日志确认收到消息并执行了展示逻辑,避免把消息未送达当成通知权限问题。

targetSdk 决定谁控制权限弹窗

Android 13(API 33)及以上引入通知运行时权限。普通通知在全新安装后默认关闭。对于目标 API 33 及以上的应用,应由应用在合适的用户操作后主动请求;调用 createNotificationChannel() 或 notify() 不会代替这一步。

如果应用仍以 API 32 或更低为目标,系统控制弹窗时机:首次创建通知渠道后启动 Activity,或先启动 Activity 再创建第一个渠道时,系统会展示权限提示。这也说明,单纯在后台创建渠道,不一定立刻看到弹窗。

旧目标版本还有一个容易干扰排查的条件:用户点过一次“不允许”后,系统不会再次提示,直到卸载重装或应用更新为目标 API 33 及以上。卸载会清除应用数据,不能要求真实用户用这种方式修复;开发时也应使用隔离测试安装,保留升级场景的数据。

检查最终清单,再显式请求

下面的示例使用 compileSdk >= 33、targetSdk >= 33、minSdk >= 23,并使用 AndroidX Activity、Core。先在清单的 manifest 内、application 外声明:

<uses-permission
    android:name="android.permission.POST_NOTIFICATIONS" />

构建变体、库清单或合并规则都可能影响最终结果。查看 Android Studio 的 Merged Manifest,确认声明实际进入待安装的包;不要只看某个源码清单。

下面是可独立放入 Activity 的请求示例。它用一个按钮演示用户主动启用通知的入口,只负责请求和反馈;正式应用应结合“提醒我进度”等功能说明用途,取得许可后再执行自己的通知展示逻辑。

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.Button
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat

class NotificationPermissionActivity : ComponentActivity() {
    private val requestNotifications = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        val text = if (granted) "通知权限已允许" else "未开启通知"
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(Button(this).apply {
            text = "启用通知"
            setOnClickListener { enableNotifications() }
        })
    }

    private fun enableNotifications() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
            ContextCompat.checkSelfPermission(
                this, Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestNotifications.launch(
                Manifest.permission.POST_NOTIFICATIONS
            )
            return
        }

        val enabled = NotificationManagerCompat.from(this)
            .areNotificationsEnabled()
        val text = if (enabled) "应用通知已开启" else "请检查系统通知设置"
        Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
    }
}

Activity 还需按项目的导航方式注册到清单中。示例没有在权限回调中立即发送通知,也没有把“点击按钮”当成已经授权;launch() 是异步请求,业务应根据回调结果继续。

用户拒绝后保留其他可用功能,不要在每次进入页面时循环请求。shouldShowRequestPermissionRationale() 返回 true 时,适合先解释用途再由用户决定是否重试;返回 false 也可能是首次申请,不能凭这一项就断定“永久拒绝”。结合应用记录的请求经历和实际权限状态判断,确需用户去设置时,提供主动点击的设置入口。

权限允许后,检查具体渠道

在 API 26 及以上,普通通知还需要已有的通知渠道,NotificationCompat.Builder(context, channelId) 使用的 ID 必须与创建的渠道一致。示例中的 CHANNEL_ID 应与应用发通知处共用同一常量:

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build

private const val CHANNEL_ID = "task_updates"

fun ensureTaskChannel(context: Context) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val manager = context.getSystemService(
            NotificationManager::class.java
        )
        manager.createNotificationChannel(
            NotificationChannel(
                CHANNEL_ID,
                "任务进度",
                NotificationManager.IMPORTANCE_DEFAULT
            )
        )
    }
}

创建渠道是幂等操作,但用同一个 ID 再创建一次,不能把用户关闭的渠道重新打开,也不能随意提高已创建渠道的重要性。应用级通知开关已开而某类消息仍没有时,读取 getNotificationChannel(CHANNEL_ID),检查是否存在、importance 是否为 IMPORTANCE_NONE,并在系统设置中核对对应渠道。

如果通知能在通知栏找到,只是没有横幅或声音,继续检查渠道重要性、勿扰模式及设备设置;这种症状与“完全不能发布通知”不同。不要通过不断更换渠道 ID 绕过用户选择。

没弹窗并不总是失败

设备从 Android 12L 或更早升级时,符合条件的已有应用可自动获得通知权限:应用已有通知渠道,而且用户之前没有关闭其通知。从旧设备恢复也可能按已有应用处理。因此“没有弹权限框但能正常发通知”可以是正常状态。

相反,旧系统中已明确关闭的通知,不会因为升级 Android 13 自动恢复。用户把权限框划走也不会改变原权限状态。应根据权限和开关的实际值判断,不要用弹窗是否出现作为唯一证据。

前台服务还有单独边界:启动 FGS 本身不以获得 POST_NOTIFICATIONS 为前提,但仍必须按要求提供前台服务通知。用户拒绝通知权限后,相关提示可以出现在任务管理界面,而不出现在通知抽屉;不要删除 startForeground() 所需的通知来“适配拒绝”。媒体会话、自管理通话等豁免有各自条件,普通业务通知不能据此跳过申请。

修复后分别核验全新安装允许、拒绝、取消,以及覆盖升级且通知原本开启、关闭的场景。允许后应能在正确渠道找到通知;拒绝后应用应正常处理未开启状态。不要清空用户数据来制造一次成功弹窗,再把它当成升级路径已经解决。