Как правильно настроить AlarmManager в Android для выполнения задачи в точное время, даже если приложение закрыто? Пример на Kotlin
Проблема и правильный подход
Ваш код действительно содержит несколько ошибок, характерных для начинающих разработчиков. Главные проблемы:
- Устаревший API:
setRepeatingначиная с Android 4.4 работает неточно, а на Android 12+ полностью заблокирован для фоновых приложений. - Отсутствие разрешений: Для работы AlarmManager в фоне на Android 12+ нужно разрешение
SCHEDULE_EXACT_ALARM. - Неправильная регистрация BroadcastReceiver: На Android 8+ неявные BroadcastReceiver (зарегистрированные только в манифесте) не работают для системных событий, но для вашего кастомного ресивера это пока не критично.
- Нет обработки Doze Mode: На Android 6+ устройство может переходить в режим энергосбережения.
Решение: используем setExactAndAllowWhileIdle + JobIntentService
Вот рабочий пример для Kotlin, который будет работать на Android 8+ и даже в Doze Mode:
// 1. Создаем BroadcastReceiver
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Здесь можно запустить службу для долгой работы
Intent(context, AlarmService::class.java).also {
context.startForegroundService(it)
}
}
}
// 2. Создаем Foreground Service для выполнения работы
class AlarmService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Создаем канал уведомлений (обязательно для API 26+)
createNotificationChannel()
val notification = NotificationCompat.Builder(this, "alarm_channel")
.setContentTitle("Будильник")
.setContentText("Время выполнить задачу!")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
startForeground(1, notification)
// Здесь ваш код: например, показать уведомление пользователю
showUserNotification()
// Останавливаем службу после выполнения
stopSelf()
return START_NOT_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"alarm_channel",
"Будильник",
NotificationManager.IMPORTANCE_HIGH
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(channel)
}
}
private fun showUserNotification() {
// Показываем уведомление пользователю
val builder = NotificationCompat.Builder(this, "alarm_channel")
.setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentTitle("Сработал будильник!")
.setContentText("Нажмите, чтобы открыть приложение")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
val pendingIntent = PendingIntent.getActivity(
this, 0, Intent(this, MainActivity::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
builder.setContentIntent(pendingIntent)
NotificationManagerCompat.from(this).notify(100, builder.build())
}
override fun onBind(intent: Intent?): IBinder? = null
}
// 3. Устанавливаем будильник (вызывается из Activity)
fun setDailyAlarm(context: Context) {
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
val intent = Intent(context, AlarmReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// Устанавливаем время на 9:00
val calendar = Calendar.getInstance().apply {
set(Calendar.HOUR_OF_DAY, 9)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
// Если время уже прошло, добавляем один день
if (before(Calendar.getInstance())) {
add(Calendar.DAY_OF_MONTH, 1)
}
}
// Проверяем разрешение (для Android 12+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (alarmManager.canScheduleExactAlarms()) {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
} else {
// Если нет разрешения, используем неточный будильник
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
} else {
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
calendar.timeInMillis,
pendingIntent
)
}
}
Что нужно добавить в манифест:
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application ...>
<receiver android:name=".AlarmReceiver" />
<service android:name=".AlarmService"
android:foregroundServiceType="specialUse" />
</application>
Важные моменты:
- setExactAndAllowWhileIdle — будит устройство из Doze Mode и срабатывает точно.
- Foreground Service нужен, так как BroadcastReceiver живёт максимум 10 секунд.
- Разрешение SCHEDULE_EXACT_ALARM нужно запрашивать у пользователя на Android 12+.
- Для повторения каждый день — просто переустанавливайте будильник заново в AlarmReceiver при срабатывании.
Как переустановить будильник на следующий день:
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// Запускаем службу
Intent(context, AlarmService::class.java).also {
context.startForegroundService(it)
}
// Переустанавливаем будильник на завтра
setDailyAlarm(context) // та же функция, что выше
}
}
Этот подход гарантирует работу на всех версиях Android от 6.0 до 14.0 (2024 год). Не забудьте также обработать перезагрузку устройства — для этого добавьте ресивер на BOOT_COMPLETED.
Альтернативный подход: WorkManager для простых задач
Если ваша задача не требует точного срабатывания в 9:00 с точностью до секунды (погрешность ±15 минут допустима), рекомендую использовать WorkManager. Это современная библиотека от Google, которая автоматически учитывает Doze Mode, перезагрузки и версии API.
// Добавьте зависимость в build.gradle
// implementation "androidx.work:work-runtime-ktx:2.9.0"
class DailyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
// Показываем уведомление
showNotification()
return Result.success()
}
private fun showNotification() {
// ... код уведомления как в первом ответе
}
}
// Устанавливаем ежедневную задачу
fun scheduleDailyTask(context: Context) Ответов пока нет.
Войдите чтобы оставить ответ
Блоги