Coding agent почти починил мне CameraX задержкой. Почему я его остановил

В Android-приложении над которым я работаю есть сканер штрихкодов на CameraX. На планшете обнаружился странный баг: если перевернуть устройство на 180°, картинка камеры на экране остаётся правильной, а кадры, которые получает анализатор, оказываются перевёрнутыми.

То есть одна камера в один момент времени как будто имеет две ориентации:

Camera ├── Preview → пользователь видит всё правильно └── ImageAnalysis → кадр перевёрнут на 180°

Сам баг оказался не особенно большим. Интереснее было то, как мы его искали.

Последнее время я практически всю разработку веду через coding agents. В этой задаче агент сам работал с проектом, менял код, запускал adb, писал logcat, анализировал результаты и обновлял MR.

У меня оставалась одна операция, которую он пока не мог выполнить: физически переворачивать планшет.

Получилось довольно необычное парное программирование.

«Запускай запись, я переверну экран»

Один из циклов отладки выглядел буквально так:

Я: запускай запись я запущу экран Агент: Запускаю чистую запись CameraXBarcodeCamera. Запись начата. Я: экран запущен Агент: Записано displayRotation=1, imageRotation=0, кадр 1920×1080... Я: запускай запись я переверну экран Агент: Начинаю новую чистую запись для переворота. Запись начата. Я: перевернул

Агент запускал adb logcat на подключённом планшете, получал данные, менял instrumentation и снова просил меня повторить эксперимент.

Это уже довольно далеко от сценария «скопировал ошибку в ChatGPT и попросил написать код».

Фактически мы строили обычный инженерный цикл:

гипотеза → изменение диагностики → эксперимент на устройстве → данные → следующая гипотеза

Только большую часть механических операций выполнял агент.

И тут появилось «давайте немного подождём»

На одном из этапов агент обнаружил интересную вещь.

После физического переворота callback изменения display уже приходил, но previewView.display.rotation в этот момент всё ещё содержал старое значение.

Примерно через полсекунды значение становилось правильным.

Вывод агента был таким:

> post недостаточен. Нужно дождаться фактической смены > previewView.display.rotation перед rebinding.

И вот здесь я остановил работу.

Я ещё не знал правильного решения. Не знал, нужен ли другой listener, надо ли менять настройки ImageAnalysis или проблема вообще находится глубже в обработке YUV.

Но «значение обновляется чуть позже, поэтому давайте его подождём, а потом перебиндим камеру» для меня выглядело как red flag.

Сегодня это полсекунды. На другом планшете --- сколько? А что именно мы ждём? Почему вообще для поворота камеры нужно пересоздавать use cases?

Я попросил ничего больше не исправлять и вместо этого доказать причину.

Не «почини», а «покажи, где расходится состояние»

Мы добавили в диагностику несколько значений:

Configuration.orientation displayRotation Preview.targetRotation ImageAnalysis.targetRotation ImageProxy.rotationDegrees

И повторили эксперимент.

До переворота получили:

Configuration.orientation=2 displayRotation=1 analysisTargetRotation=1 imageRotation=0

После переворота:

Configuration.orientation=2 displayRotation=3 analysisTargetRotation=1 imageRotation=0

Вот здесь баг практически перестал быть загадкой.

Планшет физически перешёл:

ROTATION_90 → ROTATION_270

Но для Android оба положения остаются landscape:

ORIENTATION_LANDSCAPE → ORIENTATION_LANDSCAPE

Поэтому Configuration.orientation как был 2, так и остался.

Сам display уже знал, что теперь его rotation равен 3. А ImageAnalysis продолжал жить со старым:

display ROTATION_270 ImageAnalysis.target ROTATION_90

ImageProxy.rotationDegrees вслед за этим тоже оставался 0.

Теперь у нас была не гипотеза «похоже, CameraX иногда запаздывает», а конкретная измеренная причина: при перевороте на 180° ImageAnalysis.targetRotation не обновлялся.

Решение без ожиданий и rebind

После этого исправление стало заметно проще первоначального workaround.

Было направление примерно такого вида:

DisplayListener → callback → display.rotation ещё старый → post → всё ещё старый → подождать → получить новый rotation → rebind CameraX

Стало:

OrientationEventListener → calculatedRotation → Preview.targetRotation = calculatedRotation → ImageAnalysis.targetRotation = calculatedRotation

Существующие Preview и ImageAnalysis при этом не пересоздаются.

Никаких:

delay postDelayed polling unbindAll() bindToLifecycle()

Ещё обнаружилась неочевидная деталь: градусы, которые сообщает OrientationEventListener, нельзя напрямую трактовать как одноимённый Surface.ROTATION_*. В нашем случае диапазон 45..134° должен был приводить к ROTATION_270, а не к ROTATION_90.А затем снова тот же физический эксперимент.

До:

displayRotation=1 previewTargetRotation=1 analysisTargetRotation=1 imageRotation=0

Переворачиваю планшет.

После:

orientationDegrees=70..79 calculatedRotation=3 displayRotation=3 previewTargetRotation=3 analysisTargetRotation=3 imageRotation=180

То есть цепочка наконец стала согласованной.

CameraX получил новый targetRotation, а ImageProxy.rotationDegrees изменился с 0 на 180.

И всё это без пересоздания Preview/ImageAnalysis и без rebind.

Но ведь агент сначала предложил неправильное решение

Да. И для меня это как раз самая интересная часть этой маленькой задачи.

Можно посмотреть на историю и сказать: «AI ошибся, значит ему нельзя доверять».

Можно сделать противоположный вывод: «AI всё починил».

Мне не подходит ни один.

Агент сделал огромную часть работы. Он читал существующий код, добавлял диагностику, собирал данные с устройства, запускал adb, анализировал логи, менял реализацию, повторно проверял результат и в конце обновил MR.

Я действительно почти не занимался механическим написанием этого исправления.

Но в один момент агент начал оптимизировать не ту модель проблемы: раз rotation появляется позже --- значит надо научиться его ждать.

Моя роль была не в том, чтобы быстрее него написать правильные пять строк Kotlin. Я их в тот момент даже не знал.

Моя роль была сказать: нет, сначала объясни, почему это происходит.

После этого направление расследования изменилось.

Если грубо разделить работу, получилось так:

Coding agent почти починил мне CameraX задержкой. Почему я его остановил

Что я из этого вынес

Мне кажется, разговор о coding agents часто сводится к вопросу: «может ли AI написать этот код вместо программиста?»

На практике для меня всё интереснее.

В этой задаче ценность агента была не в генерации нескольких строк с OrientationEventListener. Ценность была в том, что цикл:

найти код → добавить instrumentation → запустить adb → собрать логи → изменить реализацию → снова проверить → обновить MR

можно было практически полностью отдать ему.

А моя работа сместилась уровнем выше:

поставить эксперимент → оценить гипотезу → заметить плохое направление → потребовать доказательства → проверить результат

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

Мне всё меньше приходится быть человеком, который вручную выполняет каждый технический шаг. Но необходимость понимать, какой эксперимент мы сейчас проводим, что именно он доказывает и можно ли доверять полученному решению, никуда не исчезла.

Пожалуй, именно так для меня сейчас и выглядит разработка с coding agents.