You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:09:53