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 шт):
Пока пробую без 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>