SetAlarmClock(). Нужен рабочий, актуальный и проверенный пример использования

Я делаю приложение будильник на Android. Вроде бы я всё делаю по документации, но при этом будильник так и не срабатывает. Я не хочу использовать SetExact метод установки сигнала, потому что, судя по документации, SetAlarmClock логичнее использовать в моём случае. Мне нужен пример использования, который сегодня актуален и работает.

Вот, на всякий случай, приведу примеры кода, который я написал - интенты устанавливаются, но не срабатывают.

Класс, для установки сигналов:


class AlarmManagement private constructor(private val context: Context) {

    companion object {
        private var instance: AlarmManagement? = null

        @JvmStatic
        fun getInstance(context: Context): AlarmManagement {
            if (instance == null) {
                instance = AlarmManagement(context.applicationContext)
            }
            return instance!!
        }
    }

    val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val database = Database.getInstance(MyApplication.getAppContext())


    fun setOrUpdateAlarm(clock: Clock) {

        fun clockValuesAreCorrectForAlarmSetting(clock: Clock): Boolean {
            if(clock == null) {
                Camp.log("error alarm", "В setOrUpdateAlarm() был передан clock равный нулю")
                return false
            }
            if (clock.isActive == false) return false//это допустимо, просто завершаем метод

            if (clock.id == null) {
                Camp.log("error alarm", "Для установки сигнала был передан clock с id = null")
                return false
            }
            return true
        }
        if (!clockValuesAreCorrectForAlarmSetting(clock)) return
        if (!exactAlarmIsAllowed()) return

        val alarmIntent = Intent(context, AlarmReceiver::class.java).apply {
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            clock.id!!.toInt(),//ALARM_REQUEST_CODE
            alarmIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        //Чтобы для установки времени использовать не милиссекунды, а время в удобном формате, используется Calendar
        val calendar = Calendar.getInstance().apply {
            set(Calendar.HOUR_OF_DAY, clock.triggeringHour)
            set(Calendar.MINUTE, clock.triggeringMinute)
            if (clock.alarmRepeatingMode == AlarmRepeatingMode.EVERYDAY || clock.alarmRepeatingMode == AlarmRepeatingMode.SELECTDAYS) {
                add(Calendar.DAY_OF_YEAR, 1) // Установка интервала на день
            }
        }
        val info = AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingIntent)
        //Предупреждение: на android ниже 5.0 вместо setAlarmClock нужно использовать ExactAlarm (делаю приложение для более новых версий)
        alarmManager.setAlarmClock(info, pendingIntent)
        Camp.log("alarm", "Был установлен AlarmClock для $clock")
    }

    //Используется при запуске приложения для проверки разрешения на установку точных Alarms (AlarmClock и ExactAlarm)
    fun exactAlarmIsAllowed(): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            if (alarmManager.canScheduleExactAlarms() == true) {
                return true
            } else {
                Camp.log(
                    "alarm info",
                    "Результат проверки: отсутствует разрешение для запуска будильников (exact alarms)"
                )
                return false
            }
        } else {
            Camp.log(
                "alarm info",
                "Результат проверки: присутствует разрешение для запуска будильников (exact alarms)"
            )
            return true
        }
    }

    //Перезапускает интенты всех активных будильников
    fun reloadAllActiveClocksAlarmIntents() {
        val clocks = database.getAllClocks()
        clocks.forEach {
            setOrUpdateAlarm(it)
        }
    }

    /**    Отмена AlarmIntent по intentRequestCode
    Я использую clock.id одновременно как requestCode*/
    fun cancelAlarmIntent(intentRequestCode: Int) {
        //Создание нового пустого интента с тем же requestCode, что и у того которого нужно отменить
        //При этом происходит замена (удаление предыдущего)
        val alarmIntent = Intent(context, AlarmReceiver::class.java).apply {}
        val pendingIntent = PendingIntent.getBroadcast(
            context,
            intentRequestCode,
            alarmIntent,
            PendingIntent.FLAG_UPDATE_CURRENT
        )
        //Отмена пустого интента
        pendingIntent.cancel()
    }
}

Класс Receiver, который их принимает при срабатывании (интент с SetAlarmClock() не даже не принимается, хотя SetExact принимался). Наверное его код здесь приводить не обязательно, потому что до него интент не доходит, но всё-таки:


class AlarmReceiver : BroadcastReceiver() {

    /*    val alarmsManager = AlarmManagement.getInstance(*//*MyApplication.getAppContext()*//*)*/
    lateinit var triggeringClock: Clock

    val intentProcessingCompletionMessage = "Обработка интента завершена. "

    // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
    override fun onReceive(context: Context, intent: Intent) {

        if (intent.action == "ACTION_START_ALARM") {
            Camp.log("alarm","AlarmReceiver received alarm intent")

            val calendar = Calendar.getInstance()
            val today = calendar.get(Calendar.DAY_OF_WEEK)
            val database = Database.getInstance(MyApplication.getAppContext())

            fun intentProcessing() {

                fun gettingClock(): Clock? {

                    val intentClockId = intent.getSerializableExtra("clockId") as Long
                    if (intentClockId == null) {
                        Camp.log("error alarm", "Intent clock id == null")
                        return null
                    }

                    val dbClock = database.getClockById(intentClockId)
                    if (dbClock == null) {
                        Camp.log("error alarm", "Не найден clock c тем же id, что у intentClock")
                        return null
                    }

                    return dbClock

                }

                val clock = gettingClock()
                if (clock == null) {
                    Camp.log(
                        "error alarm",
                        "При получении объекта будильника возникла ошибка. $intentProcessingCompletionMessage"
                    )
                    return
                }

                if(!clock.isActive){
                    Camp.log(
                        "error alarm",
                        "По непредвиденной причине сработал неактивный будильник. Интент данного будильника отменён и больше не будет срабатывать. $intentProcessingCompletionMessage"
                    )
                    return
                }

                fun todayIsRightDayForAlarm(): Boolean {
                    if (clock.alarmRepeatingMode == AlarmRepeatingMode.ONETIME || clock.alarmRepeatingMode == AlarmRepeatingMode.EVERYDAY) {
                        return true
                    }
                    if (clock.alarmRepeatingMode == AlarmRepeatingMode.SELECTDAYS) {
                        //Calendar.DAY_OF_WEEK неделя начинается с ВС(вс == 1, пн == 2 и т.д), поэтому преобразуем в (1 shl (today - 1))
                        return (clock.triggeringWeekDays?.and((1 shl (today - 1)))) != 0 //проверяем наличие сегодняшнего дня в комбинации triggeringWeekDays при помощи побитового умножения
                    }
                    Camp.log(
                        "error alarm",
                        "Проверка на день недели для срабатывания данного типа будильника не была предусмотрена"
                    )
                    return false
                }
                if (!todayIsRightDayForAlarm()) {
                    Camp.log(
                        "Alarm info",
                        "Текущий день недели не соответствует дням срабатывания принятого будильника. $intentProcessingCompletionMessage"
                    )
                    return
                }
                fun activateAlarm(triggeringClock: Clock) {

                    when (triggeringClock.alarmRepeatingMode) {
                        AlarmRepeatingMode.ONETIME -> {
                            triggeringClock.isActive = false
                            database.insertOrUpdateClock_IdOrResult(triggeringClock)
                        }
                        AlarmRepeatingMode.EVERYDAY, AlarmRepeatingMode.SELECTDAYS -> {
                           
                        }
                    }

                    Camp.log("Alarm info", "Запуск срабатывания будильника. Начат запуск LockScreenActivity. $intentProcessingCompletionMessage")
                    MyApplication.getInstance().startLockScreenActivity(triggeringClock)
                }
                activateAlarm(clock)
                return
            }
            intentProcessing()
        }

        //Срабатывает при перезарузке устройства
        if (intent.action == "android.intent.action.BOOT_COMPLETED") {
            AlarmManagement.getInstance(MyApplication.getAppContext()).reloadAllActiveClocksAlarmIntents()
        }
    }
}

Манифест:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!--Для использования ExactAlarms и SetAlarmClock-->
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />

    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

    <!--Нужно, чтобы AlarmClock продолжал работу даже после перезагрузки устройства-->
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

    <!--Разрешение отображение всплывающих окон (activity)qa при фоновой работе приложения-->
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

    <uses-permission android:name="android.permission.VIBRATE" />
    <application
        android:name=".MyApplication"

        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/Base.Theme.PasswordAlarmClock"
        tools:targetApi="31">

<!-- Объявляем класс Worker для работы WorkManager -->
        <service
            android:name=".MyWorker"
            android:exported="false"
            android:permission="android.permission.BIND_JOB_SERVICE">
            <intent-filter>
                <action android:name="androidx.work.Worker" />
            </intent-filter>
        </service>
<!-- ... -->

<!--        Объявление неубиваемого сервиса для точных уведомлений-->
        <!--android:foregroundServiceType=""//я не знаю какой тип нужно использовать и обязательно ли это-->
        <service
            android:name=".AlarmService" />

<!--        <receiver
            android:name=".AlarmReceiver"
            android:enabled="true"
            android:exported="true" />-->
        <receiver android:name=".AlarmReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </receiver>


        <activity
            android:name=".MainActivity"
            android:exported="true">
        </activity>
        <activity
            android:name=".ClockSettingActivity"
            android:exported="false">
        </activity>
        <activity
            android:name=".SettingsActivity"
            android:exported="true">
        </activity>

<!--        android:windowSoftInputMode="stateHidden" используется для предотвращения автоматического появления клавиатуры (из-за использования невидимой зоны для поля для ввода)-->
        <activity
            android:name=".LockScreenActivity"
            android:exported="true"
            android:windowSoftInputMode="stateHidden">
            <!--Этот блок делает активити стартовой-->
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Ответы (1 шт):

Автор решения: Camporation

Пока пробую без Service. На эмуляторе работает всё, но на Xiaomi нет. Возможно можно обновлять через FLAG_UPDATE_CURRENT и тогда не нужно будет отменять при установке нового интента.

AlarmManagement


class AlarmManagement private constructor(/*private val context: Context*//*Если getInstance вызывается с разными контекстами, вы будете иметь несколько экземпляров AlarmManagement с разными контекстами.*/) {

    companion object {
        private var instance: AlarmManagement? = null

        @JvmStatic
        fun getInstance(): AlarmManagement {
            if (instance == null) {
                instance = AlarmManagement()
            }
            return instance!!
                //return requireNotNull(instance) { "AlarmManagement should be initialized before calling getInstance" }
        }
    }

    val alarmManager = MyApplication.getAppContext().getSystemService(Context.ALARM_SERVICE) as AlarmManager
    val database = Database.getInstance(MyApplication.getAppContext())

    val exactAlarmSettingStrategy: ExactAlarmSettingStrategy = SetAlarmClock()
    //val exactAlarmSettingStrategy: ExactAlarmSettingStrategy = SetExactAndAllowWhileIdle()

    //При установке, если интент уже с текущим id итак активен, то заного интент не будет запускаться
    fun setOrUpdateAlarm(clock: Clock) {

        fun clockValuesAreCorrectForAlarmSetting(): Boolean {
            if (!clock.isActive) return false//это допустимо, просто завершаем метод

            if (clock.id == null) {
                Camp.log("error alarm", "Для установки сигнала был передан clock с id = null")
                return false
            }
            return true
        }
        if (!clockValuesAreCorrectForAlarmSetting()) return
        val requestCode = clock.id!!.toInt()

        //костыль (смотрите ниже метод intentWithSameRequestCodeIsAlreadyActive())
        //если исправлю фунцию, то эту строку можно убрать
        cancelAlarmIntent(requestCode)

        val alarmIntent = Intent(/*context*/MyApplication.getAppContext(), AlarmReceiver::class.java).apply {
            action = "ALARM"
            putExtra("clockId", clock.id)
        }

        //region правильная и нужная проверка, но не работает, поэтому костыль - каждый раз отменяю интент перед тем как ставить новый
/*        fun intentWithSameRequestCodeIsAlreadyActive(): Boolean {
            val existingIntent = PendingIntent.getBroadcast(
                MyApplication.getAppContext(),
                requestCode,
                alarmIntent,
                PendingIntent.FLAG_NO_CREATE
            )

            if (existingIntent != null) {
                Camp.log("alarm", "Intent с requestCode == $requestCode уже активен.")
                return true
            } else {
                Camp.log("alarm", "Intent с requestCode == $requestCode не активен.")
                return false
            }
        }*/
        /*fun intentWithSameRequestCodeIsAlreadyActive(): Boolean {
            //Копия с документации Андроид
            val intent = Intent(MyApplication.getAppContext(), AlarmReceiver::class.java)
            val pendingIntent =
                PendingIntent.getService(MyApplication.getAppContext(), requestCode, intent,
                    PendingIntent.FLAG_NO_CREATE)

            if (pendingIntent != null) {
                Camp.log("alarm", "Intent с requestCode == $requestCode уже активен.")
                return true
            } else {
                Camp.log("alarm", "Intent с requestCode == $requestCode не активен.")
                return false
            }
        }*/
/*        if (intentWithSameRequestCodeIsAlreadyActive()) return//needed because of FLAG_IMMUTABLE*/
            //endregion

        if (!exactAlarmIsAllowed()) return

        val pendingIntent = PendingIntent.getBroadcast(
            MyApplication.getAppContext(),
            requestCode,
            alarmIntent,
            //PendingIntent.FLAG_UPDATE_CURRENT//Сообщение от Google разрабов:"Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles."
            PendingIntent.FLAG_IMMUTABLE
        )
        //Чтобы для установки времени использовать не милиссекунды, а время в удобном формате, используется Calendar
        val calendar = java.util.Calendar.getInstance().apply {
            set(Calendar.HOUR_OF_DAY, clock.triggeringHour)
            set(Calendar.MINUTE, clock.triggeringMinute)
        }
        if (calendar.timeInMillis <= System.currentTimeMillis()) {
            // Если время срабатывания уже прошло, добавляем день
            calendar.add(Calendar.DAY_OF_YEAR, 1)
        }

        exactAlarmSettingStrategy.setExactAlarm(alarmManager,pendingIntent,calendar)

        Camp.log("alarm", "Был установлен AlarmClock для $clock")
    }



    //Используется при запуске приложения для проверки разрешения на установку точных Alarms (AlarmClock и ExactAlarm)
    fun exactAlarmIsAllowed(): Boolean {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            return if (alarmManager.canScheduleExactAlarms()) {
                true
            } else {
                Camp.log(
                    "alarm info",
                    "Результат проверки: отсутствует разрешение для запуска будильников (exact alarms)"
                )
                false
            }
        } else {
            Camp.log(
                "alarm info",
                "Результат проверки: присутствует разрешение для запуска будильников (exact alarms)"
            )
            return true
        }
    }

    fun /*reloadAllActiveClocksAlarmIntents*/ activateInactiveAlarmIntentsWhoseClocksAreActive() {
        val clocks = database.getAllClocks()
        clocks.forEach {
            setOrUpdateAlarm(it)
        }
    }

    //По идеи должен обновлять только неактивные интенты, но из-за того, что я не знаю как проверить активность интента, он отменят и обновляет ВСЕ активные Clock - и с активными и с неактивными интентами
    /**    Отмена AlarmIntent по intentRequestCode
    Я использую clock.id одновременно как requestCode*/
    fun cancelAlarmIntent(intentRequestCode: Int) {
       val intent = Intent(MyApplication.getAppContext(), AlarmReceiver::class.java)

        //Копия с документации Андроид
        val pendingIntent =
            PendingIntent.getService(MyApplication.getAppContext(), intentRequestCode, intent,
                PendingIntent.FLAG_NO_CREATE)
        if (pendingIntent != null) {
            alarmManager.cancel(pendingIntent)
        }
    }
}

Receiver

package com.camporation.passwordalarmclock

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import java.util.Calendar


/**
 * Обрабатывает сработавшие Alarms
 */
class AlarmReceiver : BroadcastReceiver() {

/*    val alarmsManager = AlarmManagement.getInstance(*//*MyApplication.getAppContext()*//*)*/
    lateinit var triggeringClock: Clock

    val intentProcessingCompletionMessage = "Обработка интента завершена. "

    // This method is called when the BroadcastReceiver is receiving an Intent broadcast.
    override fun onReceive(context: Context, intent: Intent) {
        Camp.log("AlarmReceiver received intent")
        if (intent.action == "ALARM") {
            Camp.log("alarm","AlarmReceiver received alarm intent")

            val calendar = Calendar.getInstance()
            val today = calendar.get(Calendar.DAY_OF_WEEK)
            val database = Database.getInstance(MyApplication.getAppContext())

            //region GettingIntentClock

            /*            val classLoader = com.camporation.alarmclockwithpassword.Clock::class.java.classLoader*/

            //region OldIntentClock

            //Так как я не смог решить проблему того, ч то в интент передаётся null вместо объекта, то в extra я буду передавать строки или байты, а собирать объект уже здесь
            /*val intentClock =  intent.getSerializableExtra("clock") as? com.camporation.alarmclockwithpassword.Clock*/
            //endregion

            //
            //Какой смысл это делать если я могу брать clock из базы по id?
            /*            val intentClock =  Clock(
                (intent.getSerializableExtra("clock") as? Int)?.toLong(),
                intent.getSerializableExtra("clock") as Boolean,
                intent.getSerializableExtra("clock") as Boolean,
                intent.getSerializableExtra("clock") as? Long,
                intent.getSerializableExtra("clock") as? Long,
                intent.getSerializableExtra("clock") as? Long,
                intent.getSerializableExtra("clock") as? Long,
                intent.getSerializableExtra("clock") as? Long,
                intent.getSerializableExtra("clock") as? Long
            )*/


            fun intentProcessing() {

                fun gettingClock(): Clock? {

                    val intentClockId = intent.getSerializableExtra("clockId") as Long?
                    if (intentClockId == null) {
                        Camp.log("error alarm", "Intent clock id == null")
                        return null
                    }

                    val dbClock = database.getClockById(intentClockId)
                    if (dbClock == null) {
                        Camp.log("error alarm", "Не найден clock c тем же id, что у intentClock")
                        return null
                    }

                    return dbClock

                }

                val clock = gettingClock()
                if (clock == null) {
                    Camp.log(
                        "error alarm",
                        "При получении объекта будильника возникла ошибка. $intentProcessingCompletionMessage"
                    )
                    return
                }

                if(!clock.isActive){
                    Camp.log(
                        "error alarm",
                        "По непредвиденной причине сработал неактивный будильник. Интент данного будильника отменён и больше не будет срабатывать. $intentProcessingCompletionMessage"
                    )
                    return
                }

                fun todayIsRightDayForAlarm(): Boolean {
                    if (clock.alarmRepeatingMode == AlarmRepeatingMode.ONETIME || clock.alarmRepeatingMode == AlarmRepeatingMode.EVERYDAY) {
                        return true
                    }
                    if (clock.alarmRepeatingMode == AlarmRepeatingMode.SELECTDAYS) {
                        //Calendar.DAY_OF_WEEK неделя начинается с ВС(вс == 1, пн == 2 и т.д), поэтому преобразуем в (1 shl (today - 1))
                        return (clock.triggeringWeekDays?.and((1 shl (today - 1)))) != 0 //проверяем наличие сегодняшнего дня в комбинации triggeringWeekDays при помощи побитового умножения
                    }
                    Camp.log(
                        "error alarm",
                        "Проверка на день недели для срабатывания данного типа будильника не была предусмотрена"
                    )
                    return false
                }
                if (!todayIsRightDayForAlarm()) {
                    Camp.log(
                        "Alarm info",
                        "Текущий день недели не соответствует дням срабатывания принятого будильника. $intentProcessingCompletionMessage"
                    )
                    return
                }
                fun updateClockInDatabase(){
                    when (clock.alarmRepeatingMode) {
                        AlarmRepeatingMode.ONETIME -> {
                            clock.isActive = false
                            val idOrResult = database.insertOrUpdateClock_IdOrResult(clock)
                            Camp.log("Одноразовый будильник изменён: isActive = false. Он добавлен в базу с id/result: $idOrResult")
                        }
                        AlarmRepeatingMode.EVERYDAY, AlarmRepeatingMode.SELECTDAYS -> {
                            //на текущий момент для этих типов ничего ни требуется
                        }
                    }
                }
                updateClockInDatabase()
                MainActivity.currentActivity?.onDatabaseUpdatingInReceiver()

                //обновляет текущий (если он повторяющийся) и все остальные неактивные интенты с активными clocks (на случай если по непредвиденной причине они неактивны)
                AlarmManagement.getInstance().activateInactiveAlarmIntentsWhoseClocksAreActive()
                Camp.log("alarm allAlarms receiver","Receiver активировал все неактивные интенты с активными будильниками")
                fun activateAlarm() {
                    Camp.log("Alarm info", "Запуск срабатывания будильника. Начат запуск LockScreenActivity. $intentProcessingCompletionMessage")
                    MyApplication.getInstance().startLockScreenActivity(clock)
                }
                activateAlarm()
                return
            }
            intentProcessing()

            //Для повышения надёжности, при срабатывании любого будильника, все активные будильники можно заного обновлять при каждом срабатывании (хотя work manager итак должен справляться с ежедневным обновлением)
            /*alarmsManager.reloadAllActiveClocksAlarmIntents()*/
        }

        //Срабатывает при перезарузке устройства
        if (intent.action == "android.intent.action.BOOT_COMPLETED") {
            AlarmManagement.getInstance().activateInactiveAlarmIntentsWhoseClocksAreActive()
        }
    }
}

ExactAlarmSetting

interface ExactAlarmSettingStrategy {
    fun setExactAlarm(alarmManager: AlarmManager, pendingIntent: PendingIntent, calendar: Calendar)
}

class SetAlarmClock : ExactAlarmSettingStrategy {

    override fun setExactAlarm(
        alarmManager: AlarmManager,
        pendingIntent: PendingIntent,
        calendar: Calendar
    ) {
        val info = AlarmManager.AlarmClockInfo(calendar.timeInMillis, pendingIntent)
        //Предупреждение: на android ниже 5.0 вместо setAlarmClock нужно использовать ExactAlarm (делаю приложение для более новых версий)
        alarmManager.setAlarmClock(info, pendingIntent)
    }
}

class SetExactAndAllowWhileIdle : ExactAlarmSettingStrategy {

    override fun setExactAlarm(
        alarmManager: AlarmManager,
        pendingIntent: PendingIntent,
        calendar: Calendar
    ) {
        alarmManager.setExactAndAllowWhileIdle(
            AlarmManager.RTC_WAKEUP,
            calendar.timeInMillis,
            pendingIntent
        )
    }
}

Manifest

    <!--  Для использования ExactAlarms и SetAlarmClock  -->
    <!--  https://developer.android.com/develop/background-work/services/alarms/schedule#exact-permission-declare  -->
    <!--  Для работы требуется 1 из 2 SCHEDULE_EXACT_ALARM или USE_EXACT_ALARM  -->
    <!--  Для проверки наличия разрешения вызывается canScheduleExactAlarms()  -->
    <!--  Предоставляется вручную, для Android12 и выше  -->
    <!--  Может быть отозвано пользователем  -->
    <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>

    <!--  Предоставляется автоматически, доступно с Android13 и выше  -->
    <!--  Не может быть отозвано пользователем  -->
    <!--  Не могу использовать (что не критично), так как работаю с более ранними версиями Android  -->
    <!--  <uses-permission android:name="android.permission.USE_EXACT_ALARM"/>  -->


    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>

        <receiver android:name=".AlarmReceiver" android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
                <category android:name="android.intent.category.DEFAULT"/>
            </intent-filter>
        </receiver>
→ Ссылка