
Процессуальный 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 и устали пропускать уведомления — добро пожаловать.
Код (частично) открыт, совесть чиста. Ни слова без ордера, ни байта в облако.
Автор: 7SoulssS7
