Нишевый Android-будильник для сменщиков и вахтовиков: не «умный будильник», а надёжный инструмент для тех, у кого график не «5/2». Опора продукта — сменная работа (есть в любой стране), а не календарь конкретного государства.
- applicationId / namespace:
ru.titeha.shiftalarm - Стек: Kotlin + Jetpack Compose (Material 3), Room, минимум зависимостей.
- Циферблат выбора времени — наш отдельный компонент AnalogTimePicker (подключён с JitPack).
- Надёжный звонок через
AlarmManager.setAlarmClock— переживает Doze, показывает системную иконку будущего сигнала (тап по ней открывает наш экран); перепланирование после перезагрузки, обновления приложения, смены времени/часового пояса и выдачи разрешения на точные будильники (SystemRescheduleActions). Звонок не зависит от разблокировки: повторяющийся будильник звенит и на залоченном устройстве (в т.ч. при блокировке дольше суток) — следующее срабатывание берётся из device-protected плана (см.docs/DIRECT_BOOT.md). Сигнал играет foreground-сервис независимо от UI; заглушить можно с полноэкранного экрана «Стоп», кнопкой в уведомлении или смахиванием. Вибрация — запасной сигнал. Проверка готовности (разрешения) и диагностический журнал событий — см.docs/. - Обработчик «Показать будильники» (
ACTION_SHOW_ALARMS) — приложение можно назначить для показа списка будильников (виджеты часов, Ассистент). Полной замены системных часов на Android нет, это ближайшая штатная «имитация». - Несколько будильников, каждый включается/выключается отдельно.
- Режим «по дням недели» — выбор дней чекбоксами; пустой набор = разовый будильник
(с опцией «удалить после срабатывания»; правило «разовый» одно на движок и приёмник —
AlarmTimes.isOneShot). - Режим «По выходным и праздникам» (
calendar) — звонит в официальные нерабочие дни: выходные, праздники и перенесённые выходные; в рабочие дни молчит. «Выходные» считаются от рабочей недели пользователя (настройка «Рабочая неделя», по умолчанию 5/2 Пн–Пт): для 6-дневки это только воскресенье и праздники. Производственный календарь в этом режиме учитывается всегда (тумблер не показывается), отмеченные дни недели в расчёте не участвуют (при возврате в «по дням недели» не теряются); нет данных календаря на год — деградирует до выходных недели пользователя. Заменил прежнюю комбинацию «по дням недели + учитывать праздники + по выходным» (перенесена data-миграцией 6→7). - Режим «смены» — графики-ротации (пресеты 2/2, сутки/двое, день/ночь…). Цикл привязан к опорной дате; в рабочие слоты — звонок, в выходные — тишина.
- Редактор произвольного цикла — пресеты как шаблоны плюс «Свой цикл»: для каждого дня
отдельно задаётся тип (Утро/День/Ночь/Выходной — для цвета в календаре) и будильник
(вкл/выкл + время). Тип и звонок независимы: ночь можно показать без будильника, а выходной —
со звонком. Цикл хранится в
alarms.cycleSpec(сериализация —ShiftCycleCodec). - Календарь смен — наглядная цветная сетка месяца (утро/день/ночь/выходной/отпуск), точка на днях со звонком, листание по месяцам. Ночная смена через полночь показывается на сутках отработки, а звонок — вечером предыдущего дня.
- Правки календаря (подмены) — тап по дню в календаре → сделать день Утро/День/Ночь/Выходной
или «вернуть по графику». Режим «Диапазон»: тап начала + тап конца → правка на весь блок
(сдать рабочий блок пакетом или отработать выходные). Так меняешь отдельные дни, не трогая цикл.
Правки хранятся в таблице
alarm_overrides(один день → исключение, диапазон → подмена; раскладка по приоритетам движка —ScheduleOverrides). Для ночных смен «Выходной» на одиночной ночи делает умную отмену: снимает звонок, будивший на эту ночь (он на предыдущем дне), сохраняя звонок на следующую (ScheduleOverrides.cancelNight). - Периоды без будильника с типом (отпуск / больничный / отгул / за свой счёт) — на эти дни
звонок выключается, в календаре период красится по типу (зелёный / болотный / бирюзовый /
сиреневый). Тип выбирается чипами; хранится в колонке
reason(PeriodKind, без миграции). Период можно завести и из календаря (тап по дню/диапазону → тип периода), и отдельным блоком «+ Период». Больничный внутри отпуска продлевает отпуск на число дней болезни (ТК РФ,VacationSick; правило РФ — в перспективе настраиваемое в региональном модуле). - Учёт праздников (производственный календарь) — тумблер «Учитывать праздники» в режимах
«по дням недели» и «смены»: звонок глушится в официальные нерабочие дни — праздники
(переносы учитываются, если глобальная рабочая неделя пользователя равна стандартной —
настройка «Рабочая неделя»; в сменном режиме — только праздники). Прежний выбор полярности «по рабочим / по выходным» упразднён
(фаза D): сценарий «будить в дни отдыха» стал отдельным режимом «По выходным и праздникам»
(см. выше), а колонка
polarity— legacy (движок читает REST только у режимаcalendar). Движок:ProductionCalendar(статус дня),AlarmPolarity/PersonalDayResolver. Данные тянутся с официального источника (isDayOff.ru) в локальный кэш с оффлайн-fallback на встроенные РФ-2026/2027; звонок от сети не зависит (docs/HOLIDAY_CALENDAR_SOURCE.md). Мультистрана и выбор страны — впереди (точка расширенияProductionCalendars.DEFAULT_COUNTRY). Ввод периода двумя способами: диапазоном дат или «с даты на N дней». - Плашка за час до звонка — тихое уведомление (без звука): видно, что будильник скоро
зазвонит, и его можно выключить заранее прямо из плашки. Тумблер в разделе «Звонок»;
ставится отдельным точным сигналом за 60 минут и снимается вместе с будильником
(подробности —
docs/PRE_RING.md). - Рабочая неделя (настройка) — «5/2, Пн–Пт», «6-дневка, Пн–Сб», «4-дневка, Пн–Чт» или «Свой набор» (дни чипами). Влияет на расчёт «По выходным и праздникам» и на переносы выходных у будильников с праздниками; дни звонка обычных будильников не меняет. Изменение настройки сразу перепланирует будильники. Хранится в device-protected хранилище — доступна в пути звонка до разблокировки телефона.
Период отпуска глушит звонок. Что происходит с фазой сменного цикла после возвращения — задаётся переключателем «Заморозить цикл на время отпуска»:
- выключено (по умолчанию) — цикл крутится по календарю: ротация под низом идёт своим чередом, после отпуска выходишь на ту смену, что приходится «по графику»;
- включено — цикл замораживается: отпускные дни не считаются, после отпуска продолжаешь с той же смены, на которой ушёл (для индивидуального отсчёта / вахты).
Чистая логика отделена от Android — её легко покрывать юнит-тестами.
| Слой | Где | Что |
|---|---|---|
| UI | ui/, MainActivity |
Список будильников и редактор (Compose). |
| Данные | data/ |
Room: AlarmEntity, AlarmPeriod, AlarmOverride (+ DAO, AlarmRepository). |
| Движок расписания | schedule/ |
Чистый ShiftEngine (резолв смены/звонка на дату, отпуска, режимы цикла) и AlarmTimes (мост для одного будильника). Без Android и I/O. |
| Планирование | alarm/ |
AlarmScheduler (AlarmManager), AlarmReceiver (срабатывание + перепланирование), BootReceiver, AlarmService + AlarmActivity (экран звонка). |
Приоритет резолва смены на дату: исключение > временная подмена > период отпуска > базовая
ротация. Юнит-тесты движка — app/src/test/.../schedule/.
Требуется JDK 21 (подойдёт JBR от Android Studio). На этой машине:
JAVA_HOME="C:\Program Files\Android\Android Studio\jbr" \
GRADLE_USER_HOME="C:\tools\gradle" \
./gradlew.bat testDebugUnitTest assembleDebug --console=plainGRADLE_USER_HOME важен: без него тест-форк падает на кириллическом пути пользователя.
Каталог C:\tools\gradle — соглашение этой машины (рядом C:\Android\Sdk, на C: достаточно
места); готовая обёртка с этими путями: .agent/tasks/env/gradle-check.sh.
Готовый APK: app/build/outputs/apk/debug/app-debug.apk.
Инструментальный тест миграции БД (нужен подключённый телефон/устройство, не эмулятор):
./gradlew.bat :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.class=ru.titeha.shiftalarm.data.AppDatabaseMigrationTest- compileSdk / targetSdk 36, minSdk 26, JVM 21.
- Jetpack Compose (Material 3), Room (через KSP), DataStore.
- Компонент
com.github.titeha:AnalogPickerPlayground(версия — вgradle/libs.versions.toml).
Приложение выпущено (RuStore, готовится Google Play). Версия 1.1.0 — первый публичный релиз, включающий весь цикл исправлений надёжности. Известные пред-релизные TODO закрыты.
Миграции БД — настоящие (без пересоздания): схемы версий экспортируются в app/schemas/,
переходы покрыты инструментальным тестом миграции (AppDatabaseMigrationTest).
Планы (следующие итерации): производственный календарь праздников как отдельный платный модуль по странам (РФ → Казахстан → …), локализация (en, kk). Подмены по дню и диапазоном в UI — уже есть (см. «Правки календаря»). Идея на потом — «детский»/школьный профиль (учебная неделя + каникулы как периоды без звонка).