
Процессуальный Debug
Меня зовут Вячеслав, и я — «процессуальный хирург».
Сейчас адвокат. Из них 20 лет я провел по ту сторону баррикад — работал следователем, помощником прокурора и прокурором.
Моя работа в суде — не красивые речи, а поиск багов. Я берусь за дела, где система дала сбой: следствие допустило ошибку, суд закрыл глаза. Я провожу аудит материалов, нахожу фатальное нарушение (баг в процедуре) и «ломаю» приговор. Я не работаю ради процесса — я либо вижу техническую возможность отмены, либо честно говорю клиенту: «Тут WontFix».
Год назад я понял, что мне нужен инструмент, который работает так же бескомпромиссно, как я сам.
Мне нужен был цифровой ассистент, который:
Не лжет (гарантированно уведомляет о суде, даже если телефон «спит»).
Не сдает (шифрует данные так, чтобы никакой опер не вскрыл).
Рынок предложил мне красивые, но «дырявые» календари с облачной синхронизацией. Я знаю цену «облаку» — это просто чужой компьютер, к которому у следствия есть ключи.
Выйдя на пенсию решал что делать. Поэтому сделал неожиданный ход. Пошел учиться. Сначала — на «Инженера по тестированию» в Яндекс, чтобы понять, как ломается софт. А затем открыл документацию Kotlin и написал свою систему — ERRATA.
Часть 1. Почему Программирование похоже на Следствие
Когда я погрузился в IT, я был поражен. Логика кода и логика уголовного процесса практически идентичны.
УПК РФ — это Техзадание. Шаг влево, шаг вправо — процессуальная ошибка (Exception), и всё дело разваливается (App Crash).
Следствие — это Дебаггинг. Ты ищешь, где в цепочке событий произошел сбой. Кто виноват? Неправильная переменная (ложные показания) или ошибка в логике (неверная квалификация)?
Приговор — это Релиз. Момент истины, когда результат твоей работы оценивают другие.
Курс QA в Яндексе дал мне правильную оптику. Я смотрел на свой будущий продукт не как творец, а как тестировщик. Я задавал вопросы, которые обычно игнорируют джуны:
«А что, если пользователь сменит часовой пояс за час до суда?»
«А что, если память кончится во время записи вердикта?»
Именно навыки QA позволили мне, разработчику-одиночке, выстроить архитектуру так, чтобы потом не переписывать всё с нуля. Я выбрал Clean Architecture + MVVM, потому что люблю порядок. В коде, как и в уголовном деле, всё должно быть разложено по папкам, а слои ответственности не должны смешиваться.
Я написал для себя приложение-органайзер ERRATA, чтобы не пропускать судебные заседания. И тут же столкнулся с тем, что стандартные методы Android (WorkManager, обычные Alarm) просто не работают надежно. Телефон «засыпает», Samsung убивает процессы, и уведомление о суде приходит на 30 минут позже. Для юриста это фатально.
Использовать Firebase (FCM) я не мог принципиально:
Offline-First: Мое приложение хранит данные только локально (SQLite/Room), защищая адвокатскую тайну. Сервер не знает расписания.
Независимость: В условиях санкций полагаться на сервисы Google в критически важном софте — риск.
Часть 2. Главная боль: «Телефон уснул, адвокат проспал»
Мой MVP был готов через месяц. Но на первых же полевых тестах я столкнулся с проблемой, которая ставила крест на всем проекте.
Пришлось искать способ пробить защиту вендоров (Samsung/Xiaomi) штатными средствами Android SDK. Ниже — гайд по реализации «неубиваемого» будильника, который работает даже на Android 14.
Оказалось, что современные Android ради экономии батареи агрессивно убивают фоновые процессы. Режим Doze Mode превращает смартфон в кирпич, который игнорирует обычные таймеры.
Для обычного юзера это «ну, не пришел пуш от игры». Для адвоката — это вопрос репутации.
Мне нужно было решение уровня «Военная тревога». Чтобы телефон «орал», даже если он в глубоком сне. И без использования Google Services (Firebase), потому что в нынешних реалиях полагаться на них в России — риск.
Часть 3. 🛑Проблема: Почему WorkManager не подходит. Пробиваем Doze Mode
Я перепробовал всё: WorkManager (ненадежно, система может отложить выполнение), обычный AlarmManager.
Уведомления приходят с задержкой от 5 до 15 минут.
На Samsung/Xiaomi они не приходят вообще, если приложение выгружено из памяти (свайп).
Система Doze Mode игнорирует
setExact.
В итоге я нашел «священный грааль» — связку, которая работает на 100% устройств, включая Android 14.
Мы используем цепочку, которая дает максимальный приоритет процессу: AlarmManager (setAlarmClock) ➔ BroadcastReceiver ➔ Foreground Service
Почему именно так?
setAlarmClock: Единственный метод, который Android считает «настоящим будильником». Он гарантированно выводит устройство из Doze Mode (глубокого сна). Даже
setExactAndAllowWhileIdleработает хуже.Foreground Service: Обычный
WorkManagerможет быть отложен системой. Фронтальный сервис запускается мгновенно и (благодаря уведомлению) дает гарантию, что процесс не убьют, пока он читает «тяжелую» базу данных.
🛠 Реализация
1. Манифест и Права
На Android 12+ (особенно 14) право на точный будильник (SCHEDULE_EXACT_ALARM) нужно запрашивать, а USE_EXACT_ALARM выдается не всем. Но для setAlarmClock разрешения работают иначе.
XML
<manifest ...> <!-- Обязательно для точных таймеров --> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <!-- Обязательно для Android 14+ --> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <application ...> <receiver android:name=".receivers.NotificationReceiver" android:enabled="true" android:exported="false"> <!-- exported=false для безопасности --> </receiver> <service android:name=".services.ReminderService" android:foregroundServiceType="dataSync" android:exported="false" /> </application> </manifest>
2. Планировщик (AlarmScheduler)
Ключевой момент — использование setAlarmClock. Это «ядерная кнопка», которую система боится игнорировать.
kotlin
// AlarmScheduler.kt fun scheduleAlarm(context: Context, item: ScheduleItem) { val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager val intent = Intent(context, NotificationReceiver::class.java).apply { putExtra("ITEM_ID", item.id) } // Важно: FLAG_IMMUTABLE может мешать обновлению extras, используем UPDATE_CURRENT val pendingIntent = PendingIntent.getBroadcast( context, item.id.hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val alarmInfo = AlarmManager.AlarmClockInfo( item.timeInMillis, pendingIntent // Этот интент откроет приложение по клику на иконку будильника в статус-баре ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (alarmManager.canScheduleExactAlarms()) { alarmManager.setAlarmClock(alarmInfo, pendingIntent) } else { // Fallback: просим пользователя выдать права askPermission(context) } } else { alarmManager.setAlarmClock(alarmInfo, pendingIntent) } }
3. Ресивер с защитой от дребезга (Debounce)
Иногда система (особенно Samsung при разблокировке экрана) может прислать старые интенты «пачкой». Защищаемся от спама.
kotlin
// NotificationReceiver.kt class NotificationReceiver : BroadcastReceiver() { // Простой дебаунс через SharedFlow или статическую переменную companion object { private var lastTriggerTime = 0L } override fun onReceive(context: Context, intent: Intent) { val currentTime = System.currentTimeMillis() if (currentTime - lastTriggerTime < 1000) { Log.d("ERRATA", "Debounce: Skip duplicate alarm") return } lastTriggerTime = currentTime val itemId = intent.getIntExtra("ITEM_ID", -1) // Запускаем Service, так как Receiver живет всего 10 секунд val serviceIntent = Intent(context, ReminderService::class.java).apply { putExtra("ITEM_ID", itemId) } // Для Android 8+ (Oreo) обязательно startForegroundService if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(serviceIntent) } else { context.startService(serviceIntent) } } }
4. Сервис и уведомления (ReminderService)
Здесь мы читаем БД и показываем уведомление. Важный нюанс — навигация и WakeLock.
kotlin
// ReminderService.kt class ReminderService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { // 1. Сразу показываем "техническое" уведомление, чтобы сервис не убили startForeground(SERVICE_ID, createForegroundNotification()) // 2. Асинхронно идем в БД (Room) CoroutineScope(Dispatchers.IO).launch { val itemId = intent?.getIntExtra("ITEM_ID", -1) ?: return@launch val item = db.dao().getById(itemId) // 3. Показываем РЕАЛЬНОЕ уведомление с данными showUserNotification(item) // 4. Останавливаем сервис stopSelf() } return START_NOT_STICKY } // ... createForegroundNotification() ... }
Результат: На тестах из 50 срабатываний — 50 успехов. Даже на телефоне, который лежал без движения.
Часть 4. 🐛Samsung & Xiaomi Special: Грабли
Даже с идеальным кодом вендоры вставляют палки в колеса.
1. «Убийство» свайпом
На некоторых версиях OneUI, если пользователь выгрузил приложение из «Недавних» (свайп вверх), AlarmManager сбрасывается.
Решение: Программно проверять производителя (
Build.MANUFACTURER) и показывать диалог с просьбой поставить режим батареи в "Не ограничено" (Unrestricted).
2. Задержки WorkManager
Никогда не используйте WorkManager для точных уведомлений на Samsung. Он оптимизирован для экономии батареи, а не для точности. Только AlarmManager.
3. Deep Links в Compose
Если при клике на уведомление открывается пустой экран, убедитесь, что вы передаете ID как аргумент в DeepLink URI (например, myapp://detail/{id}), и в MainActivity вы обрабатываете intent в onCreate и onNewIntent.
Заключение: Больше, чем MVP.
Я не планирую становиться Junior-разработчиком в банке. Я остаюсь адвокатом, который пишет код.
Для меня программирование — это продолжение моей основной работы.
В суде я защищаю людей от системных ошибок правосудия.
В коде я защищаю коллег от системных ошибок Android и утечек д��нных.
Эта архитектура сейчас работает в продакшене моего приложения ERRATA (органайзер для адвокатов).
Тесты показали:
Samsung S24 (Android 14, One UI 6.1): Работает идеально, будит из сна.
Xiaomi (HyperOS): Работает стабильно (при наличии разрешения на Автозапуск).
Старые ведра (Android 8-10): Работают без нареканий.
Я не профессиональный разработчик, я пришел в IT из прокуратуры, чтобы решить свои задачи. Но этот опыт показал мне, что иногда «старые» инструменты (AlarmManager) работают надежнее модных (WorkManager).
ERRATA сегодня — это не сырой прототип, а система версии v1.0 Production Ready, готовая к реальной работе "в поле".
За интерфейсом приложения на Kotlin стоит надежная, хоть и невидимая пользователю инфраструктура:
Свой сервер (Node.js + SQLite), который занимается только валидацией лицензий и не хранит пользовательские данные.
Telegram-бот (Telegraf), через который реализован безопасный магазин и активация ключей. Это позволяет не зависеть от биллинга сторов и сохранять прямой контакт с пользователями.
Построен суверенный "цифровой сейф", который не зависит от Google, зарубежных облаков и капризов вендоров телефонов.
Сейчас приложение доступно в формате Direct Release (прямая установка APK). Если вы юрист с телефоном Samsung/Xiaomi и устали пропускать уведомления — добро пожаловать.
Код (частично) открыт, совесть чиста. Ни слова без ордера, ни байта в облако.
