Android 13/14 通知权限与前台服务适配指南

16 阅读5分钟

Android 13/14 通知权限与前台服务适配指南

Android 13(API 33)引入了通知运行时权限,Android 14(API 34)进一步强化了对前台服务类型的管控。本文结合实际开发中的常见问题,系统梳理这两个版本的适配要点。


一、Android 13(API 33)核心变更

1.1 通知权限(POST_NOTIFICATIONS)

背景: Android 13 之前,应用只需在 Manifest 中声明 USE_FULL_SCREEN_INTENT 即可发送通知,用户无法按应用粒度控制通知。Android 13 引入了运行时通知权限,用户可以选择拒绝接收某个应用的通知。

权限声明:

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

权限申请时机:

建议在用户能够直观理解为什么需要通知的场景下请求权限,例如用户点击"开启通知"按钮时,而不是应用启动时立即请求。

// 检查并请求通知权限(Activity 中)
private fun requestNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
        }
    }
}

// 使用 Activity Result API(推荐)
private val requestPermissionLauncher =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            // 权限已授予,可以发送通知
            showNotification()
        } else {
            // 权限被拒绝,引导用户到设置中开启
            showPermissionDeniedDialog()
        }
    }

用户拒绝后的处理:

private fun showPermissionDeniedDialog() {
    AlertDialog.Builder(this)
        .setTitle("需要通知权限")
        .setMessage("开启通知后可以及时收到消息提醒")
        .setPositiveButton("去设置") { _, _ ->
            // 跳转到应用设置页面
            val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                data = Uri.fromParts("package", packageName, null)
            }
            startActivity(intent)
        }
        .setNegativeButton("取消", null)
        .show()
}

重要行为变化:

场景行为说明
升级到 Android 13 的设备,应用已获得通知权限系统自动授予新的通知权限,无需重新申请
全新安装的应用必须主动申请,系统不会自动授予
权限被用户永久拒绝("不再询问")只能引导用户到系统设置中手动开启
调用 notify() 但没有权限通知不会显示,不会抛异常(静默失败)

1.2 精细化媒体权限

Android 13 将原来的 READ_EXTERNAL_STORAGE 拆分为三个精细化权限:

<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

兼容处理(同时支持旧版本和新版本):

<!-- 使用 maxSdkVersion 兼容旧版本 -->
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
    
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

<!-- Android 13+ 使用新权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

1.3 Wi-Fi 权限变更

Android 13 引入了 NEARBY_WIFI_DEVICES 权限,替代原来需要 ACCESS_FINE_LOCATION 才能使用 Wi-Fi API 的限制。

<!-- AndroidManifest.xml -->
<uses-permission
    android:name="android.permission.NEARBY_WIFI_DEVICES"
    android:usesPermissionFlags="neverForLocation" />

二、Android 14(API 34)核心变更

2.1 前台服务类型必须声明

变更说明: Android 14 要求所有前台服务必须在 Manifest 中显式声明 foregroundServiceType,否则会抛异常。

Manifest 配置示例:

<service
    android:name=".MyForegroundService"
    android:foregroundServiceType="location|dataSync"
    android:exported="false" />

支持的前台服务类型:

类型用途需要额外权限
location持续获取位置(如导航)ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION
mediaPlayback媒体播放
mediaProjection屏幕录制
phoneCall通话相关MANAGE_OWN_CALLS
dataSync数据同步(Android 15 有超时限制)
health健康设备数据同步HIGH_SAMPLING_RATE_SENSORS
remoteMessaging远程消息处理
systemExempted系统豁免(需审核)

2.2 动态广播注册必须显式指定导出行为

变更说明: Android 14 要求使用 registerReceiver() 注册动态广播时,必须显式指定 RECEIVER_EXPORTEDRECEIVER_NOT_EXPORTED,否则会抛 SecurityException

// ✅ 正确:显式指定导出行为
// 场景1:接收其他应用发送的广播(需要导出)
val filter1 = IntentFilter("com.example.MY_ACTION")
context.registerReceiver(myReceiver, filter1, Context.RECEIVER_EXPORTED)

// 场景2:只接收应用内部广播(不需要导出,更安全)
val filter2 = IntentFilter("com.example.INTERNAL_ACTION")
context.registerReceiver(myReceiver, filter2, Context.RECEIVER_NOT_EXPORTED)

// ❌ 错误:不指定导出行为(Android 14 上会崩溃)
context.registerReceiver(myReceiver, filter)
// java.lang.SecurityException: registerReceiver must specify exported behavior

LocalBroadcastManager 已废弃的替代方案:

// 使用 LocalBroadcastManager 的方式(已废弃)
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter)

// ✅ 推荐替代方案1:使用 RECEIVER_NOT_EXPORTED
context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)

// ✅ 推荐替代方案2:使用 LiveData / StateFlow(应用内通信)
// ✅ 推荐替代方案3:使用 SharedFlow(事件总线)

2.3 OpenJDK 17 行为变更

Android 14 核心库对齐 OpenJDK 17,以下行为需要注意:

UUID 生成变化
// Android 14 之前:UUID 生成使用随机数字
// Android 14+:UUID 生成使用了新的实现,但仍然是符合 RFC 4122 的
// 一般不影响业务,但如果依赖 UUID 的具体格式做解析,需要测试
val uuid = UUID.randomUUID().toString()
正则表达式变化
// Android 14 使用了更新的正则表达式引擎
// 某些边缘情况的正则匹配行为可能变化
// 建议对依赖正则的核心逻辑进行回归测试
val regex = Regex("\\s+")
val result = regex.replace(input, " ")

2.4 更严格的 Intent 过滤规则

Android 14 对隐式 Intent 和 Intent 过滤器加强了限制:

<!-- Android 14 要求 Intent 过滤器必须显式声明 exported -->
<receiver
    android:name=".MyReceiver"
    android:exported="true">  <!-- 必须显式声明 -->
    <intent-filter>
        <action android:name="com.example.MY_ACTION" />
    </intent-filter>
</receiver>

三、版本适配策略建议

3.1 版本判断的最佳实践

// ✅ 推荐:使用 VERSION_CODES 常量,可读性好
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    // Android 13+ 的逻辑
}

// ❌ 不推荐:直接使用数字,可读性差
if (Build.VERSION.SDK_INT >= 33) {
    // ...
}

3.2 Gradle 配置建议

// gradle.properties
android.useAndroidX=true
android.enableJetifier=true

// app/build.gradle
android {
    compileSdk 34  // 或 35,建议紧跟最新稳定版
    
    defaultConfig {
        minSdk 21
        targetSdk 34  // 逐步升级,不要跨太多版本
    }
    
    buildFeatures {
        viewBinding true
        dataBinding true
    }
}

3.3 分阶段升级建议

阶段targetSdk 版本说明
第一阶段33(Android 13)适配通知权限,风险较小
第二阶段34(Android 14)适配前台服务类型 + 广播注册
第三阶段35(Android 15)适配 Edge-to-Edge + 16KB 页面

四、常见问题 FAQ

Q:通知权限被用户拒绝后,如何再次请求?

A:如果用户只是拒绝(没有勾选"不再询问"),下次调用 requestPermissionLauncher.launch() 时会再次弹出系统对话框。如果用户勾选了"不再询问",只能通过 Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 跳转到应用设置页面,引导用户手动开启。

Q:Android 14 的前台服务类型可以设置多个吗?

A:可以。在 Manifest 中使用竖线分隔,例如 android:foregroundServiceType="location|dataSync"。但需要在启动前台服务时通过 ServiceInfo.FOREGROUND_SERVICE_TYPE_ 常量指定本次启动的具体类型。

Q:迁移到 Android 14 后,以前用 LocalBroadcastManager 的代码怎么办?

A:LocalBroadcastManager 已废弃,推荐用 RECEIVER_NOT_EXPORTED 注册广播,或者迁移到 StateFlow / SharedFlow 实现应用内事件通信。


五、参考资源


如有遗漏或错误,欢迎在评论区指正。如果本文对你有帮助,欢迎点赞收藏。