Процессуальный Debug

Меня зовут Вячеслав, и я — «процессуальный хирург».
Сейчас адвокат. Из них 20 лет я провел по ту сторону баррикад — работал следователем, помощником прокурора и прокурором.

Моя работа в суде — не красивые речи, а поиск багов. Я берусь за дела, где система дала сбой: следствие допустило ошибку, суд закрыл глаза. Я провожу аудит материалов, нахожу фатальное нарушение (баг в процедуре) и «ломаю» приговор. Я не работаю ради процесса — я либо вижу техническую возможность отмены, либо честно говорю клиенту: «Тут WontFix».

Год назад я понял, что мне нужен инструмент, который работает так же бескомпромиссно, как я сам.
Мне нужен был цифровой ассистент, который:

  1. Не лжет (гарантированно уведомляет о суде, даже если телефон «спит»).

  2. Не сдает (шифрует данные так, чтобы никакой опер не вскрыл).

Рынок предложил мне красивые, но «дырявые» календари с облачной синхронизацией. Я знаю цену «облаку» — это просто чужой компьютер, к которому у следствия есть ключи.

Выйдя на пенсию решал что делать. Поэтому сделал неожиданный ход. Пошел учиться. Сначала — на «Инженера по тестированию» в Яндекс, чтобы понять, как ломается софт. А затем открыл документацию Kotlin и написал свою систему — ERRATA.

Часть 1. Почему Программирование похоже на Следствие

Когда я погрузился в IT, я был поражен. Логика кода и логика уголовного процесса практически идентичны.

  • УПК РФ — это Техзадание. Шаг влево, шаг вправо — процессуальная ошибка (Exception), и всё дело разваливается (App Crash).

  • Следствие — это Дебаггинг. Ты ищешь, где в цепочке событий произошел сбой. Кто виноват? Неправильная переменная (ложные показания) или ошибка в логике (неверная квалификация)?

  • Приговор — это Релиз. Момент истины, когда результат твоей работы оценивают другие.

Курс QA в Яндексе дал мне правильную оптику. Я смотрел на свой будущий продукт не как творец, а как тестировщик. Я задавал вопросы, которые обычно игнорируют джуны:

  • «А что, если пользователь сменит часовой пояс за час до суда?»

  • «А что, если память кончится во время записи вердикта?»

Именно навыки QA позволили мне, разработчику-одиночке, выстроить архитектуру так, чтобы потом не переписывать всё с нуля. Я выбрал Clean Architecture + MVVM, потому что люблю порядок. В коде, как и в уголовном деле, всё должно быть разложено по папкам, а слои ответственности не должны смешиваться.

Я написал для себя приложение-органайзер ERRATA, чтобы не пропускать судебные заседания. И тут же столкнулся с тем, что стандартные методы Android (WorkManager, обычные Alarm) просто не работают надежно. Телефон «засыпает», Samsung убивает процессы, и уведомление о суде приходит на 30 минут позже. Для юриста это фатально.

Использовать Firebase (FCM) я не мог принципиально:

  1. Offline-First: Мое приложение хранит данные только локально (SQLite/Room), защищая адвокатскую тайну. Сервер не знает расписания.

  2. Независимость: В условиях санкций полагаться на сервисы 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

Почему именно так?

  1. setAlarmClock: Единственный метод, который Android считает «настоящим будильником». Он гарантированно выводит устройство из Doze Mode (глубокого сна). Даже setExactAndAllowWhileIdle работает хуже.

  2. 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 и устали пропускать уведомления — добро пожаловать.

Код (частично) открыт, совесть чиста. Ни слова без ордера, ни байта в облако.