Avatar ·

Как правильно использовать AlarmManager в Android для отложенных задач и напоминаний?

Всем привет! Я новичок в Kotlin и Android-разработке. Пишу приложение-напоминание. Нужно, чтобы приложение срабатывало в определённое время (например, каждый день в 9:00) и показывало уведомление. Я нашёл класс `AlarmManager`, но не могу понять, как его правильно настроить. Пробовал такой код: ```kotlin val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(this, ReminderReceiver::class.java) val pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val calendar = Calendar.getInstance().apply { set(Calendar.HOUR_OF_DAY, 9) set(Calendar.MINUTE, 0) set(Calendar.SECOND, 0) } alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, calendar.timeInMillis, AlarmManager.INTERVAL_DAY, pendingIntent) ``` Но он не работает. Уведомления не приходят. Я проверил манифест — добавил разрешение `RECEIVE_BOOT_COMPLETED` и зарегистрировал `BroadcastReceiver`. Что я делаю не так? Нужно ли использовать `setExactAndAllowWhileIdle`? Или проблема в версии API? Пожалуйста, объясните для новичка, как правильно настроить AlarmManager в Kotlin. Также интересует: как отменить установленный будильник? И как обрабатывать ситуацию, когда телефон перезагрузился? Буду благодарен за пример с комментариями. Код пишу в Android Studio, targetSdk 33. <<>>

Проблема и её причины

Основная ошибка в вашем коде — использование метода setRepeating(). Начиная с Android 4.4 (API 19) этот метод перестал быть точным: система может объединять повторяющиеся будильники для экономии батареи. А начиная с Android 6.0 (API 23) появился режим Doze, который ещё больше ограничивает работу AlarmManager. Кроме того, setRepeating() вообще не гарантирует точного срабатывания. Для точных повторяющихся событий нужно использовать setExactAndAllowWhileIdle() и самостоятельно переустанавливать будильник после каждого срабатывания.

Правильное решение (Kotlin + BroadcastReceiver)

Вот рабочий пример для API 23+ (Android 6.0+). Он совместим с Doze-режимом и корректно обрабатывает перезагрузку.

1. Создаём BroadcastReceiver

class ReminderReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Здесь показываем уведомление
        showNotification(context, "Напоминание", "Не забудьте сделать дело!")
        // Переустанавливаем будильник на следующий день
        setDailyAlarm(context)
    }

    private fun showNotification(context: Context, title: String, message: String) {
        val channelId = "reminder_channel"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Напоминания",
                NotificationManager.IMPORTANCE_HIGH
            )
            val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            manager.createNotificationChannel(channel)
        }

        val notification = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(android.R.drawable.ic_dialog_info)
            .setContentTitle(title)
            .setContentText(message)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setAutoCancel(true)
            .build()

        val notificationManager = NotificationManagerCompat.from(context)
        notificationManager.notify(1, notification)
    }
}

2. Функция установки будильника (в Activity или Application)

fun setDailyAlarm(context: Context) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, ReminderReceiver::class.java)
    val pendingIntent = PendingIntent.getBroadcast(
        context,
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )

    // Вычисляем время ближайшего срабатывания
    val calendar = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, 9)
        set(Calendar.MINUTE, 0)
        set(Calendar.SECOND, 0)
        set(Calendar.MILLISECOND, 0)
        // Если текущее время уже прошло 9:00, устанавливаем на завтра
        if (before(Calendar.getInstance())) {
            add(Calendar.DAY_OF_YEAR, 1)
        }
    }

    // Используем setExactAndAllowWhileIdle для точности даже в Doze
    alarmManager.setExactAndAllowWhileIdle(
        AlarmManager.RTC_WAKEUP,
        calendar.timeInMillis,
        pendingIntent
    )
}

3. Регистрация в манифесте

<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Для Android 13+ -->

<application ...>
    <receiver android:name=".ReminderReceiver"
        android:exported="false">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
    </receiver>
</application>

4. Обработка перезагрузки

Создайте второй BroadcastReceiver, который будет переустанавливать будильник после загрузки системы:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            setDailyAlarm(context)
        }
    }
}

Зарегистрируйте его в манифесте аналогично, указав android.intent.action.BOOT_COMPLETED.

5. Отмена будильника

fun cancelAlarm(context: Context) {
    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val intent = Intent(context, ReminderReceiver::class.java)
    val pendingIntent = PendingIntent.getBroadcast(
        context,
        0,
        intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
    )
    alarmManager.cancel(pendingIntent)
    pendingIntent.cancel()
}

Важные замечания

  • Для Android 12+ обязательно используйте флаг FLAG_IMMUTABLE в PendingIntent.
  • Для Android 13+ не забудьте запросить разрешение POST_NOTIFICATIONS в рантайме.
  • Если ваше приложение работает на Android 12 и выше, добавьте в манифест <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> и проверьте наличие разрешения через alarmManager.canScheduleExactAlarms().
<<>>

Альтернативный подход: WorkManager

Если вам не нужна абсолютная точность (допустима задержка в несколько минут), рекомендую использовать WorkManager вместо AlarmManager. WorkManager гораздо проще в настройке, автоматически учитывает Doze-режим и перезагрузки устройства, а также не требует ручного управления BroadcastReceiver.

Пример с WorkManager

// В build.gradle добавьте зависимость:
// implementation "androidx.work:work-runtime-ktx:2.8.1"

class DailyReminderWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        // Показываем уведомление
        showNotification(applicationContext, "Напоминание", "Пора действовать!")
        return Result.success()
    }
}

// Запланировать ежедневное напоминание:
val dailyRequest = PeriodicWorkRequestBuilder<DailyReminderWorker>(
    24, TimeUnit.HOURS // интервал
).setInitialDelay(calculateDelayUntil(9, 0), TimeUnit.MILLISECONDS)
 .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "daily_reminder",
    ExistingPeriodicWorkPolicy.KEEP,
    dailyRequest
)

// Функция для расчёта задержки до 9:00
fun calculateDelayUntil(hour: Int, minute: Int): Long {
    val now = Calendar.getInstance()
    val target = Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, hour)
        set(Calendar.MINUTE, minute)
        set(Calendar.SECOND, 0)
        if (before(now)) add(Calendar.DAY_OF_YEAR, 1)
    }
    return target.timeInMillis - now.timeInMillis
}

Преимущества

  • Не нужно писать BroadcastReceiver для перезагрузки.
  • Работает на всех версиях Android, начиная с API 14.
  • Автоматически адаптируется к режимам энергосбережения.

Недостаток: минимальный интервал для периодической работы — 15 минут (для точных ежедневных задач это не проблема). Если вам нужно точное срабатывание в конкретное время, используйте OneTimeWorkRequest и перепланируйте