2023年Android如何实现精准本地通知调度?
Android 精准本地通知调度问题(2023年)
核心需求
截至2023年,Android是否支持触发误差在±60秒内的精准本地通知?我开发的是一款提醒类APP,需要在传感器指定时段无事件发生时发送本地通知。
当前问题
我用AlarmManager实现了延迟通知,但设置较长延迟(比如1小时)时,通知只有在打开APP后才会显示,和2022年某Stack Overflow未解决的问题情况一致。另外看到有文章说AlarmManager和WorkManager都不是理想方案,想确认这个需求是否可实现,以及具体操作方法。
测试代码
MainActivity.kt
测试设备为Samsung A14,UI包含申请通知权限和调度通知两个按钮,showDelayedNotification是AlarmManager的设置逻辑:
package com.example.localnotificationstest import... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val notify = NotificationSetup(applicationContext) notify.createNotificationChannel() setContent { LocalNotificationsTestTheme { val context = LocalContext.current var hasNotificationPermission by remember { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { mutableStateOf( ContextCompat.checkSelfPermission( context, Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED ) } else mutableStateOf(true) } val permissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), onResult = {isGranted -> hasNotificationPermission = isGranted}) MainUI(context, permissionLauncher, hasNotificationPermission) } } } } @Composable fun MainUI(context: Context, perm: ActivityResultLauncher<String>, hasPermission: Boolean) { val notificationHelper = NotificationHelper(context) Column( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Button(onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { perm.launch(Manifest.permission.POST_NOTIFICATIONS) } }) { Text(text = "Request Permission") } Button(onClick = { if (hasPermission) { //showNotification(context) showDelayedNotification(context) } }) { Text(text = "Show Notification") } } } fun showNotification(context: Context) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_ID) .setSmallIcon(R.drawable.ic_background_search) .setPriority(NotificationCompat.PRIORITY_HIGH) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle("Snuff this notification") .setContentText("Hello this is your local notification") notificationManager.notify(NotificationHelper.NOTIFICATION_ID, builder.build()) } class NotificationBroadcastReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (context != null) { showNotification(context) } } } fun showDelayedNotification(context: Context) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val notificationIntent = Intent(context, NotificationBroadcastReceiver::class.java) val broadcast = PendingIntent.getBroadcast( context, 100, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) alarmManager.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 3600000L, broadcast) } class NotificationSetup(private val context: Context) { fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = "NotificationTest" val descriptionText = "Used for testing notification stuff" val importance = NotificationManager.IMPORTANCE_HIGH val channel = NotificationChannel( NotificationHelper.CHANNEL_ID, name, importance ).apply { description = descriptionText } val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannel(channel) } } } class NotificationHelper(private val context: Context) { companion object { const val CHANNEL_ID = "counter_channel" const val NOTIFICATION_ID = 1 } }
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.localnotificationstest" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SET_ALARM" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.LocalNotificationsTest" tools:targetApi="31"> <receiver android:name=".NotificationBroadcastReceiver" android:enabled="true" android:exported="false" /> <activity android:name=".MainActivity" android:exported="true" android:label="@string/app_name" android:theme="@style/Theme.LocalNotificationsTest"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
解决方案
1. 需求可行性确认
你的需求(±60秒误差)完全可以实现,但需要针对Android版本和厂商定制系统做适配,核心基于AlarmManager的精准API,同时处理系统休眠和电池优化限制。
2. 当前代码优化点
- 电池优化白名单适配:
Android 12及以上系统,即便申请了权限,若应用在电池优化列表内,AlarmManager的精准调度仍会被延迟。可通过以下代码引导用户将应用加入白名单:fun checkBatteryOptimization(context: Context) { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager if (!powerManager.isIgnoringBatteryOptimizations(context.packageName)) { val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { data = Uri.parse("package:${context.packageName}") } context.startActivity(intent) } } - 精准Alarm API选择:
你当前使用的setExact在设备休眠时可能无法触发,Android 12(API 31)及以上建议改用setExactAndAllowWhileIdle,该API允许设备空闲时触发闹钟:fun showDelayedNotification(context: Context) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val notificationIntent = Intent(context, NotificationBroadcastReceiver::class.java) val broadcast = PendingIntent.getBroadcast( context, 100, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val triggerTime = System.currentTimeMillis() + 3600000L if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, broadcast) } else { alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, broadcast) } } - WorkManager 精准调度适配:
若用WorkManager,默认调度不精准,需开启精准配置(依赖SCHEDULE_EXACT_ALARM权限):val workRequest = OneTimeWorkRequestBuilder<NotificationWorker>() .setInitialDelay(1, TimeUnit.HOURS) .setConstraints(Constraints.Builder() .setRequiresBatteryNotLow(true) .build()) .setInputData(Data.Builder().putString("key", "value").build()) .build() WorkManager.getInstance(context).enqueue(workRequest)
3. 厂商定制系统适配
三星、小米、华为等厂商有独立电池优化策略,即便完成上述配置,应用后台仍可能被限制。需在APP内添加引导,提示用户手动开启应用的后台活动权限或自启动权限,并跳转至对应系统设置页面。
4. 传感器场景适配
针对传感器无事件触发通知的需求,可选择两种方案:
- 用
ForegroundService持续监听传感器事件,需申请FOREGROUND_SERVICE权限并显示前台通知,避免应用被系统杀死。 - 结合
AlarmManager定期唤醒应用,检查传感器事件记录,若指定时段内无事件则发送通知。
内容的提问来源于stack exchange,提问作者llamahairy
相关产品推荐
相关产品推荐

