Avatar ·

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

Всем привет! Я новичок в разработке под Android на Kotlin и никак не могу разобраться с AlarmManager. Мне нужно, чтобы приложение отправляло уведомление каждый день в 9:00 утра, даже если оно полностью закрыто (свайпнуто из списка последних). Я написал такой код, но он не работает, когда приложение убито: ```kotlin val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(this, MyReceiver::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) ``` BroadcastReceiver у меня зарегистрирован в манифесте: ```xml ``` Проблема в том, что на Android 8+ (API 26+) уведомления не приходят, если приложение было закрыто. Я читал, что `setRepeating` устарел, но не понимаю, чем его заменить. Также я слышал про `setExact` и `setAlarmClock`, но запутался в их различиях. Пожалуйста, объясните на простом примере, как правильно настроить AlarmManager для точного повторяющегося события, которое будет работать на современных версиях Android (API 26+). Важно: приложение должно просыпать устройство, если оно в спячке. Заранее спасибо! <<>>

Проблема и правильный подход

Ваш код действительно содержит несколько ошибок, характерных для начинающих разработчиков. Главные проблемы:

  • Устаревший 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)