PunchSowore demands release of detained FIJ journalist OjukwuInquirerBukidnon water district gears up as Super El Niño threatens supplyCNN TürkUzmanı açıkladı: Akciğer kanserinde hastalığın evresi tedavi başarısını etkiliyorDaily MaverickBaltic nations briefly issue drone alerts as Russia, Ukraine exchange strikesThe Jerusalem PostNeria Leiter, son of Israeli envoy to US, discharged from hospital, transferred to rehab centerZDF heuteAktuelle Pressemitteilungen des ZDFKhaosod EnglishGiant boulder falls near Phatthalung home after days of heavy rainDeadline‘NAZA’ Becomes One Of The Most Watched Docs On Theatrical Release In 2026GIGAZINEGoogle製高性能AI「Gemini 4 Carbon」が一般公開される、複数のAIが協力する新機能で3Dゲームを制作するデモも披露Observador DesportoEurodeputados debatem recentes ataques informáticos à UE20 MinutenThomas Müller postet erstes Pärchen-FotoBBC NewsMan Utd open talks with Pochettino about replacing Carrick
The Daily Newsstand · Free, Always
Wednesday, November 11, 2026

Будильник, который точно звонит: что я узнал о точных напоминаниях на Android, пока делал планер

Translate

Я делал планер дня под Android. Его главная фишка простая: занятие начинается — телефон напоминает. Утром — будильник. Казалось бы, AlarmManager существует с первой версии Android, что тут сложного?

На деле оказалось, что «напомнить в 07:00» и «напомнить примерно около 07:00, если телефон не против» — это два разных API. Ниже — всё, на что я наступил, с кодом и цифрами с реальных телефонов (Realme GT Neo 2 на ColorOS и OnePlus 15R на Android 16).

1. Почему уведомления опаздывают

Причин три, и все они работают одновременно. Первая — Doze: когда экран выключен и телефон лежит неподвижно, система копит будильники приложений и выполняет их пачкой в «окнах обслуживания», причём окна сначала частые, а потом всё реже. Вторая — App Standby Buckets: чем реже человек открывает приложение, тем ниже его «корзина» и тем реже системе разрешено его будить. Третья — оболочки производителей: поверх Android у Realme, Xiaomi, Huawei и других есть свои «оптимизаторы батареи».

Замеры это подтвердили. setExactAndAllowWhileIdle в принудительном глубоком Doze на Realme пришёл на 134 секунды позже. На ColorOS напоминания любых сторонних приложений — не только моего, но и TickTick, и WhatsApp — получали окно до часа, даже когда приложению в настройках разрешена «работа без ограничений»; это политика оболочки, и обойти её из приложения я не нашёл как. Ровно в минуту на обоих телефонах звонил только setAlarmClock.

2. Три уровня точности

AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);

// 1) «Когда-нибудь около» — система вправе сдвинуть и сгруппировать
am.set(AlarmManager.RTC_WAKEUP, at, pi);

// 2) Точно, в том числе в Doze, но с ограничением частоты
am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, at, pi);

// 3) Будильник: Doze его не откладывает
am.setAlarmClock(new AlarmManager.AlarmClockInfo(at, showPi), pi);

За каждый уровень приходится платить. setExactAndAllowWhileIdle в Doze срабатывает не чаще, чем примерно раз в несколько минут на приложение (точные цифры зависят от версии Android), но для «напомнить в начале занятия» этого хватает: занятия не идут каждую минуту.

setAlarmClock честнее всех: система выходит из Doze заранее, чтобы будильник прозвенел вовремя. Зато в строке состояния появляется значок будильника, а на экране блокировки — время следующего. Поэтому в планере это отдельный режим: «уведомление» или «настоящий будильник» выбирает человек, а не я за него.

Итоговое правило у меня такое:

Что

Чем ставим

Подъём, «не пропусти»

setAlarmClock

Напоминание о начале занятия или дела

setExactAndAllowWhileIdle

Обновление виджета

setExact без пробуждения (RTC, не RTC_WAKEUP)

3. Разрешения: Android 12, 13, 14

Android 12+ — точные будильники. Нужен SCHEDULE_EXACT_ALARM (человек может его отозвать) или USE_EXACT_ALARM (его не отзывают, но Google Play разрешает его только приложениям, где будильник или календарь — основная функция). Проверять обязательно, иначе setExact… бросит SecurityException:

static boolean canExact(Context ctx) {
    AlarmManager am = (AlarmManager) ctx.getSystemService(Context.ALARM_SERVICE);
    if (Build.VERSION.SDK_INT >= 31) return am.canScheduleExactAlarms();
    return true;
}

if (canExact(ctx)) am.setAlarmClock(new AlarmManager.AlarmClockInfo(at, show), fire);
else am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, at, fire);   // хуже, но не падаем

Android 13+ — POST_NOTIFICATIONS. Я сначала спрашивал разрешение при первом запуске — когда человек ещё не понимает, зачем оно. Теперь вопрос появляется карточкой в момент, когда человек сам включает напоминания и понимает, о чём речь.

Android 14+ — USE_FULL_SCREEN_INTENT. Без него будильник при заблокированном экране не разворачивается на весь экран, а приходит обычным уведомлением. Это разрешение тоже стали выдавать не всем, поэтому проверяем и при необходимости ведём человека в настройки:

NotificationManager nm = ctx.getSystemService(NotificationManager.class);
if (Build.VERSION.SDK_INT >= 34 && !nm.canUseFullScreenIntent()) {
    Intent i = new Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT,
            Uri.parse("package:" + ctx.getPackageName()));
    activity.startActivity(i);
}

4. Звонок — не в уведомлении, а в сервисе

Первая версия играла звук самим уведомлением: канал с высоким приоритетом, мелодия будильника. Работало, пока тестировщик не написал: «Нажал „Готово", а телефон продолжает вибрировать». Звук и вибрация уведомления живут своей жизнью, и надёжно погасить их из кода нельзя.

Решение — короткоживущий foreground service, который сам играет звук и вибрирует, а уведомление только показывает кнопки:

// AlarmService: звук через поток будильника — его громкость отдельная, и он звучит в режиме «Не беспокоить»,
// если человек разрешил будильники
MediaPlayer mp = new MediaPlayer();
mp.setAudioAttributes(new AudioAttributes.Builder()
        .setUsage(AudioAttributes.USAGE_ALARM)
        .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
        .build());
mp.setDataSource(this, alarmUri);
mp.setLooping(true);
mp.prepare();
mp.start();

// вибрация с той же «ролью», иначе в беззвучном режиме её срежут
AudioAttributes aa = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_ALARM).build();
vibrator.vibrate(VibrationEffect.createWaveform(PATTERN, 0), aa);

// не даём процессору уснуть, но и не звоним вечно
wake.acquire(MAX_RING_MS + 5000);           // MAX_RING_MS = 10 минут
handler.postDelayed(this::stopRinging, MAX_RING_MS);

Сервис стартует с типом FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK (Android 10+), в манифесте — FOREGROUND_SERVICE и FOREGROUND_SERVICE_MEDIA_PLAYBACK. Кнопки «Выключить» и «Отложить» в уведомлении и на полноэкранном окне вызывают stopRinging() в том же процессе — и звук с вибрацией гаснут сразу.

Уведомление при этом:

new NotificationCompat.Builder(this, CH)
        .setCategory(NotificationCompat.CATEGORY_ALARM)
        .setPriority(NotificationCompat.PRIORITY_MAX)
        .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
        .setFullScreenIntent(fullPi, true)   // окно будильника поверх экрана блокировки
        .setOngoing(true)
        .addAction(0, "Выключить", dismissPi)
        .addAction(0, "Отложить 10 мин", snoozePi)
        .build();

Грабля, на которую ушёл вечер: раз звук играет сервис, хочется сделать уведомление «тихим» через setSilent(true). Не делайте так: тихое уведомление не запускает full-screen intent, и окно будильника просто не появляется. Тишину лучше задать каналом без звука.

5. Перезагрузка и обновление приложения

Все будильники AlarmManager пропадают при перезагрузке. Это известно, но есть нюансы:

<receiver android:name=".BootReceiver" android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
    </intent-filter>
</receiver>

Сам список будильников я храню в SharedPreferences: приёмник берёт его оттуда и ставит всё заново, не дожидаясь, пока человек откроет приложение. MY_PACKAGE_REPLACED нужен потому, что обновление приложения тоже снимает его будильники: без этого человек обновился вечером — и утром будильник молчит.

У этого есть обратная сторона: обновление может «воскресить» то, что новая версия уже не хочет ставить. У меня старые версии сами включали будильник подъёма, новая перестала, но после обновления приёмник восстановил старый список раньше, чем человек открыл приложение, и будильник прозвенел. Пришлось один раз чистить сохранённый список прямо в приёмнике.

И ещё одна тонкость: BOOT_COMPLETED приходит только после первой разблокировки после перезагрузки. Если телефон перезагрузился ночью сам (обновление системы), утренний будильник не прозвенит, пока его не разблокируют. Лечится через LOCKED_BOOT_COMPLETED и хранение списка в device-protected storage — так у меня теперь и сделано.

6. Часовые пояса

Если хранить будильник как «07:00 по местному времени», а ставить в AlarmManager абсолютное время в миллисекундах, после перелёта всё уедет на разницу поясов. Я пересчитываю и переставляю напоминания один раз при смене пояса: человек, прилетевший из Москвы в Бангкок, хочет вставать в 07:00 по-бангкокски, а не в 11:00.

7. Как это проверять

Ждать сутки, пока телефон уснёт сам, не нужно:

# какие будильники стоят у приложения
adb shell dumpsys alarm | grep -A3 com.example.app

# загнать телефон в глубокий Doze прямо сейчас
adb shell dumpsys battery unplug
adb shell dumpsys deviceidle force-idle

# вернуть как было
adb shell dumpsys deviceidle unforce
adb shell dumpsys battery reset

Из практики добавлю пару советов. Сначала убедитесь через dumpsys alarm, что тестовый будильник действительно поставлен, и только потом включайте force-idle. Если приложение переставляет напоминания при каждом открытии (у меня так), оно сотрёт ваши тестовые записи, поэтому тестовое напоминание лучше добавлять «по-честному» — как обычное занятие в плане — и потом удалять. И проверяйте на телефонах с разными оболочками: чистый Android и ColorOS ведут себя очень по-разному.

Итог

Если свести всё вместе, то, что должно прозвенеть «ровно», ставьте через setAlarmClock, а остальное — через setExactAndAllowWhileIdle. Проверяйте canScheduleExactAlarms() и имейте запасной путь без исключения. POST_NOTIFICATIONS спрашивайте в момент, когда человек понимает зачем, а на Android 14+ проверяйте ещё и canUseFullScreenIntent().

Звук и вибрацию будильника играйте в foreground service с USAGE_ALARM, а не в уведомлении, и не используйте setSilent() на уведомлении с full-screen intent. Восстанавливайте будильники на BOOT_COMPLETED и MY_PACKAGE_REPLACED и подумайте о LOCKED_BOOT_COMPLETED; при смене часового пояса их тоже нужно переставлять. А принудительный Doze через adb экономит дни тестов.

Буду рад, если в комментариях расскажете, как с этим справляются Xiaomi и Samsung — у меня под рукой были только Realme и OnePlus.

Всё описанное работает в моём планере дня «Ритм» (Android, бесплатный).

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.