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 | Я |
|---|---|
| Читал код | Формулировал эксперимент |
| Добавлял логи | Работал с физическим планшетом |
Запускал adb/logcat | Оценивал гипотезы |
| Анализировал значения | Остановил решение через ожидание |
| Менял реализацию | Потребовал доказать root cause |
| Проверял новые логи | Определял критерий корректного решения |
| Обновил MR | Принимал результат |
Что я из этого вынес
Мне кажется, разговор о coding agents часто сводится к вопросу: «может ли AI написать этот код вместо программиста?»
На практике для меня всё интереснее.
В этой задаче ценность агента была не в генерации нескольких строк с OrientationEventListener. Ценность была в том, что цикл:
найти код
→ добавить instrumentation
→ запустить adb
→ собрать логи
→ изменить реализацию
→ снова проверить
→ обновить MR
можно было практически полностью отдать ему.
А моя работа сместилась уровнем выше:
поставить эксперимент
→ оценить гипотезу
→ заметить плохое направление
→ потребовать доказательства
→ проверить результат
При этом coding agent не стал безошибочным. Наоборот, этот кейс хорошо показал, почему бездумное «почини баг» может закончиться вполне работающим postDelayed, который потом проживёт в проекте несколько лет.
Мне всё меньше приходится быть человеком, который вручную выполняет каждый технический шаг. Но необходимость понимать, какой эксперимент мы сейчас проводим, что именно он доказывает и можно ли доверять полученному решению, никуда не исчезла.
Пожалуй, именно так для меня сейчас и выглядит разработка с coding agents.