The Jerusalem PostLod mayor ‘seething’ after Jewish-Arab clashes, right-wing activist summoned for questioningInquirerIlocos Sur town sets P180 minimum farmgate price for live hogsPunchDon’t politicise sports, Presidency tells Obi over Flamingos greetingDaily MaverickRENEWABLES CRISIS : Pele’s governance test: Why a private boardroom matters to SA’s energy transitionZDF heuteEntdecken Sie das ZDF-NachrichtenstudioSRF NewsSchauspieler und Comedian – Über zehn Jahre Haft für Russell BrandIl Fatto QuotidianoLe renne che nuotano per la migrazione invernale: le incredibili immagini che arrivano dalla Norvegia – VIDEOCollider‘The Acolyte’ Director’s New Thriller Officially Scores ‘The Wheel of Time’ StarScreen RantPlayStation Officially Announces All-New ConsoleDigital Spy"What is going on?": Former Strictly star Oti Mabuse hits out at the judges' scoringCBS NewsHow to qualify for credit card debt forgiveness in 2027ABC NewsRussell Brand sentenced to 10 years 6 months in prison for rape, sexual assault
The Daily Newsstand · Free, Always
Monday, November 9, 2026

Часть 2. Своя оболчка вместо АА? А почему бы и нет?

Translate

В первой части я рассказал, как подружил китайскую магнитолу с Baidu CarLife и Android Auto. Headswap притворяется для ГУ телефоном с CarLife, а перед Android Auto наоборот ГУ. Пятнадцать историй про особенности чужих автомобильных прошивок.

С тех пор прошёл месяц, я откатал первую 1000 км на своей Honda Vezel, а приложение заполучило своё лицо. В версии 4.0 на экране ГУ теперь можно выбрать не только AA, но и свой рабочий стол. Свой хост для автомобильных версий 2ГИС и Яндекс Навигатора, Алиса по кнопке голосового помощника и любые приложения телефона.

Как это получилось? Пришлось копаться в чужих APK, строить допущения, не соглашаться с теми, кто уже что-то подобное сделал. Главная интрига в конце: автомобильные навигаторы Яндекса и 2ГИС пускают наш хост только потому, что проверка хоста у них либо отключена, либо обходится одним самообъявленным разрешением. Надеюсь когда кто-нибудь из команды Яндекс Навигатора прочитает это, то не закроет лазейку. Ведь теперь весь китайский импорт ездит на их картах и говорит с Алисой :)

Код по-прежнему открыт (GPL-3.0), репозиторий на GitHub.

Часть 1. Хронология

Дата

Версия

Что появилось

8 окт

1.0

Мост AA → CarLife по USB. Перекодирование видео, повтор кадра каждые 100 мс, сервер AA пользователь включает руками

14 окт

1.1.11

Первый релиз на GitHub, автообновление из приложения

15 окт

2.0.0

Беспроводной CarLife (бета), масштаб AA 100–280 dpi

24 окт

2.6.0

Удалена служба специальных возможностей

2 ноя

3.0.0

Прямой проброс видео по умолчанию, свой IDR, SPS-кроп

3 ноя

3.1.0

Вертикальные экраны. В тот же день 4.0 влита в main одним PR на +20 тысяч строк

6 ноя

4.0.0

Рабочий стол, хост Car App, Алиса, голос навигатора

8 ноя

4.1.0

Свои цвета стола, Алиса/Gemini только голосом, портретные приложения в окне, фикс Android 17, арабский язык

9 ноя

4.1.1

Разделение путей Pixel и Samsung на Android 17

От 3.0 до 4.1.1 - неделя и 292 коммита, в пиковый день их было 59. Kotlin вырос с 27,6 до 48,2 тысяч строк без тестов, тесты ещё 16,9 тысяч. Более 10 тыс скачиваний и 2200 подписчиков на момент публикации.

Часть 2. От 1.0 к 2.0: обрастаем машинами

Версия 1.0 от 8 октября умела только одно - взять видео и звук Android Auto и переложить их в CarLife по USB. Через несколько дней в чате были сотни машин: Mazda, VW, Hyundai, Toyota и пр. Причём каждая со своей магнитолой и своими причудами…

С 1.0 к 2.0:

  • Беспроводной CarLife (2.0.0, 15 октября). "По воздуху работает?", "А можно без шнурка?" Да, но CarLife по воздуху - это не один TCP-сокет, а шесть отдельных, плюс Wi-Fi Direct, плюс RFCOMM по Bluetooth для обмена параметрами сети. Беспровод открыл половину новых историй из первой статьи: Mazda, которая разбирает Wi-Fi-группу на паузе, VW, который перегревается от беспроводной зарядки. Но без него проект остался бы игрушкой для одной машины.

  • Масштаб интерфейса (100–280 dpi). Один и тот же поток AA на 7-дюймовой и на 12-дюймовой магнитоле выглядит по-разному, поэтому внедряю dpi, который мы объявляем в ServiceDiscovery.

  • Служба специальных возможностей и её удаление (2.6.0, 24 октября). Сервер Android Auto на 127.0.0.1:5277 одноразовый: любое неаккуратное закрытие сессии его глушит, а штатно перезапустить его может только тот, кто откроет меню разработчика, три кнопки и нажмёт «Остановить/Вкл сервер ГУ». Сначала это делал сам Headswap через службу специальных возможностей, кликая по кнопкам за пользователя. Работало, но некоторые пользователи не хотели разрешать такие права для приложения (на Android 13+ это ещё и квест с «ограниченными настройками»), а кто-то просто не ставил приложение из-за него. В 2.6.0 службу выпилил насовсем. Сервер теперь включается руками один раз и больше не гасится. Для этого пришлось научить Headswap не трогать сессию 5277 без нужды и завершать её вежливым ByeBye, который сервер переживает.

Отдельный стоит отметить то, как вообще понять, что сервер AA жив. Казалось бы, что проще: перед настоящим подключением сделать пробную и открыть сокет на 5277. Убедиться, что отвечает, и закрыть. Оказалось, что так нельзя. Сам сервер головного устройства внутри Android Auto оказался удивительно хрупким, и любая неправильная «проба» его калечит:

  • Голый connect и сразу close (как делает любой сканер портов) сервер принимает, но VERSION_RESPONSE не присылает. А после примерно пятидесяти таких подключений у него начинаются таймауты — и всё, порт отвечает, а рукопожатие не идёт. Четыре nc -z подряд на стенде — и сервер приходится перезапускать руками.

  • Короткая проба «прошёл рукопожатие → тут же разорвал» тоже вредит: следующее нормальное подключение получает 27 секунд ECONNREFUSED. То есть мы своей проверкой «а работает ли сервер» сами же его и ломаем на полминуты.

Мне помог подписчик. Выяснилось: вредна не короткость сессии как таковая, а обрыв посреди её настройки. Короткая, но полная сессия с открытыми каналами, выставленным видеофокусом и вежливым ByeBye через секунду после AuthComplete начал проходить шесть раз из шести и сервер не трогает. Так что «пробник» в Headswap - это не connect/close, а полноценное мини-подключение, которое аккуратно здоровается и так же аккуратно прощается. Эта же механика «правильного прощания» потом легла в основу того, почему после удаления спецвозможностей сервер живёт неделями и переживает по пять «стопов» за поездку.

Часть 3. От 2.0 к 3.0: чего стоил прямой проброс

Это самый важный переход, и он дался тяжелее всего. Сначала я провёл опрос среди пользователей, хотел приделать свой старый проект тоже про машины в него. Но собрал гору хейта) и тележку хотелок. Однако, внезапно тот же подписчик предложил разобраться с проблемой прямого проброса видео потока. Он натолкнул на мысль, что есть волшебная метадата в H.264 в SPS, которая может закрыть вопрос без перекодирования.

Проблема. В 1.0–2.x видео всегда перекодировалось: декодер Android Auto → OpenGL → энкодер с настройками «как у Baidu». Это гарантировало, что любая магнитола переварит поток. Но перекодирование постоянно нагружало процессор телефона. У людей грелись батареи, процессор уходил в тротлинг, а на слабых телефонах картинка начинала дёргаться. H.264 на входе и на выходе один и тот же...

Почему это не бесплатно. Прямой проброс сразу упёрся в то, что декодеры магнитол гораздо капризнее, чем казалось. Причём я сорвал джек-пот так называемых “чёрных экранов” на своей Vezel. Wizcar и т.п. свистки просто не решали эту проблему. Одна моя подписчица на Civic-е жаловалась как раз на неё, однако производитель тупо игнорил все обращения. Сейчас она счастлива как никогда)))

Отзыв на Wizcar

Отзыв на Wizcar

Отзыв

Отзыв

Именно поэтому когда я увидел чёрный экран в первый раз на своей ГУ просто реализовал перекодирование))

Итак, главная история про Honda-ы: в режиме прямого проброса (видео от АА идёт как есть в Байду КарЛайф) после камеры заднего хода или штатного меню экран становился чёрным. Тач проходит, Android Auto жив, звук идёт, а картинки нет. Оказалось, магнитола молча сбрасывает свой декодер и ждёт следующий ключевой кадр (IDR), но никакого сигнала об этом не шлёт. Другие ГУ в тех же ситуациях исправно шлют VIDEO_ENCODER_PAUSE/START, и телефон по сигналу отдаёт свежий IDR. Honda же сбрасывает декодер молча, поэтому чёрный экран. А Android Auto в режиме проброса отдаёт IDR примерно раз в минуту (мне удалось найти проект на github где чуваки замерили - оказалось раз в 69 сек), а на статичной картинке раз в десять минут. Ну вы поняли))

Свой IDR. Восемь тестовых сборок ушли мимо. Мягко попросить ключевой кадр у Android Auto не вышло. Он игнорил мои запросы. Полный цикл переключения фокуса давал IDR, но с мерцанием и потерей нажатий. Первая более-менее версия получилась только по касанию на экран запрашивать ключевой кадр и дисплей начал оживать. Однако это было ошибкой, т.к. любые другие касания провоцировали “дёрганья”, запрашивая опять IDR. В итоге пришла идея (не без подсказки из патентов про вставку ключевого кадра в поток на лету): а давайте подмешивать свой IDR в этот поток? Headswap параллельно декодирует поток Android Auto и раз в 2 секунды собирает из последней картинки собственный ключевой кадр, который вставляет в поток. Без потерь он не влезает (0,9–2,4 МБ при буфере магнитолы около 560 КБ), поэтому кадр кодируется макроблоками I16×16 со сжатием, четырьмя слайсами параллельно, QP подбирается в пределах 18–40. Чуть мыльно, но через 2 секунды его сменят обычные кадры. Сработало! Тот же подписчик выразился так:

Свой IDR

Свой IDR

Коварная побочка. Наш IDR добавлялся в поток лишним кадром. Я это заметил только вечером в 30 минутной поездке: задержка отклика на тач ушла до 5–6 секунд, а после заднего хода вдруг становилась мгновенной (очередь то сбрасывалась). Магнитола показывает кадры с постоянной частотой, поэтому каждый лишний кадр - это плюс одна позиция в её очереди. Если бы я не запомнил, что именно делал с телефоном в тот момент, искал бы причину очень долго. Лечение оказалось достаточно простым «выбросываем P+1 кадр»: наш IDR не добавляется к потоку, а заменяет собой два кадра Android Auto. Всё работает!

Отдельная песня с широкими экранами. Раньше поля на экранах 1920×720 резал перекодировщик. В 3.0 это делает SPS-кроп: правим поля frame_crop_* прямо в заголовке потока, и обрезку выполняет декодер магнитолы бесплатно. Нюанс: Honda применяет кроп снизу и справа, но игнорирует смещение сверху, поэтому мы просим Android Auto рисовать интерфейс в левом верхнем углу (поле ui_config.margins, найденное декомпиляцией AA), а поля класть справа и снизу.

Результат. В 3.0.0 (2 ноября) прямой проброс стал режимом по умолчанию. На Vezel 42 минуты по кабелю: задержка стабильно около секунды, за поездку выброшено около 970 кадров, картинка после заднего хода возвращается мгновенно. Перекодирование осталось как запасной вариант для магнитол, которые иначе не умеют (как выяснилось потом, почти все корейские марки поставляются со слабыми ГУ, у кого-то вообще Android 4.2.2 KitKat!!!, а у Пежо самое дно). И вот только после того, как видео научилось ходить напрямую и стабильно, стало возможным следующее: а зачем нам вообще Android Auto посередине?

Часть 4. Почему свой “лаунчер”?

Бесспорно Android Auto является хорошо сделанным протокол, но это чужая витрина, и она диктует правила. А правила созданы, чтобы их нарушать))) Один из главных запросов пользователей - просмотр Youtube роликов (настоятельно рекомендую во время движения смотреть только на дорогу и не воспроизводить видео). Особенно у наших братьев из ближнего востока.

  • Какие приложения показывать решает Google. В сетку AA попадают только приложения с автомобильной частью. Сторонние видеоплееры вроде Fermata и зеркало экрана AA показывает, только если мы представляемся «Google Desktop Head Unit»; стоит указать в ServiceDiscovery реальную марку машины и видеоприложения пропадают. Обычный плеер без категории CAR_LAUNCHER/CATEGORY_PROJECTION не появится никогда.

  • Интерфейс решает Google. Сетка иконок, Coolwalk, размеры плиток. На широком 1920×720 и на старом 800×480 будет одно и то же, подвинуть ничего нельзя.

  • Цепочка из двух протоколов - это два места отказа: сервер на 127.0.0.1:5277, который глохнет от неосторожного закрытия сессии, плюс сам CarLife.

Идею своего лаунчера я рассматривал ещё 16 октября и тогда отложили, решив, что «лаунчером становится сетка AA». Вернули её с Ильдаром Садыковым, который форкнул проект 30 октября, выкинул Android Auto и вывел на экран магнитолы собственный рабочий стол на виртуальном дисплее. Он скинул мне, и 2 ноября я проверил его сборку на своей Vezel: картинка есть, тач работает. Через сутки ядро дисплея из форка переехало в основной проект, а Ильдар на экран «Авторы».

Дальше всё это пришлось довести до состояния, когда оно работает не только на моей машине…Потому что у меня всё работало начиная с версии 0.1.0 :)

Часть 5. Виртуальный дисплей без root и без спецвозможностей

Что нужно

На словах просто. Создаём VirtualDisplay, поверхность которого вход аппаратного H.264-энкодера. Запускаем на нём свой лаунчер и любые приложения. Кадры энкодера идут в тот же конвейер CarLife, который раньше гонял видео AA, так что все находки первой статьи (нарезка по 16 КиБ, потолок IDR, кроп через SPS, heartbeat на паузе) работают и здесь без изменений.

На деле обычному приложению такой дисплей не дадут. Чтобы на нём запускались чужие активности, чтобы туда можно было инжектить касания и чтобы клавиатура вылезала там, а не на телефоне, нужны системные разрешения: ADD_TRUSTED_DISPLAY, INJECT_EVENTS, INTERNAL_SYSTEM_WINDOW, MANAGE_ACTIVITY_TASKS, REAL_GET_TASKS и ещё с полдюжины. Ни одного из них у приложения из магазина нет.

Тупики

  • MediaProjection (штатная запись экрана) даёт картинку, но не тач в чужие приложения. Касания пришлось бы инжектить через службу специальных возможностей, ту самую, которую я выпилил в 2.6.0, а на Android 13+ это ещё и квест с «ограниченными настройками». Назад дороги нет.

  • Root отсекает почти всю аудиторию.

  • Shizuku - ещё одно приложение, которое надо поставить и в котором надо разобраться.

Решение: instrumentation от имени shell

У shell (adb shell) почти все нужные разрешения уже есть. И у Android <17 есть штатный механизм, которым пользуются автотесты am instrument. Instrumentation-класс запускается внутри процесса приложения, но от имени запустившего, и через UiAutomation.adoptShellPermissionIdentity() временно берёт разрешения shell.

Активация в итоге выглядит так:

am instrument --no-hidden-api-checks -w <пакет>/.DelegatedDesktopInstrumentation

Instrumentation живёт, пока жив процесс: держит доверенный дисплей и выполняет команды основной части приложения. Команды он принимает только с токеном, который выдало само приложение, чтобы этим каналом не воспользовался кто-то посторонний. Флаг --no-hidden-api-checks снимает для процесса ограничения на скрытые API, отдельных библиотек обхода не требуется.

Запустить am instrument можно с компьютера по USB, но хотелось без компьютера. В Android 11+ есть «Отладка по Wi-Fi»: телефон подключается adb-клиентом сам к себе. Поэтому я юзаю библиотеку Kadb, чтобы пользователь один раз вводил код сопряжения из настроек разработчика, и дальше активация в одну кнопку.

Цена решения: после перезагрузки телефона активацию надо повторять. Instrumentation умирает вместе с процессом, автозапуска у него нет. Это главное неудобство 4.0, но хорошо записанное видео с инструкцией творит чудеса))

Грабли

  • Режим зеркала физического экрана (--screen) на Android 16 падает: из SurfaceControl убрали getPhysicalDisplayToken. Остались только виртуальные дисплеи. В 17 ещё веселее))

  • Звук. Приложения на виртуальном дисплее играют как обычно: в динамик телефона или по Bluetooth. Чтобы отдать звук в CarLife по USB, нужен захват. REMOTE_SUBMIX без AudioMix давал тишину; сделал loopback-микс по списку usage (медиа, навигация, ассистент) с правами захвата, которые instrumentation тоже берёт у shell.

  • Голос навигатора молчал. Лог показал четыре активных плеера, все выводятся в A2DP «Honda HFT». Навигаторы проговаривают подсказки с usage = NAVIGATION_GUIDANCE, а этого значения не было в списке захвата. Лечилось одной константой.

  • Потом выяснилось, что «весь звук по Bluetooth» (одно время это был режим по умолчанию) работает не везде: на Toyota RAV4 с магнитолой Desay A2DP рвётся при старте CarLife. Выбор USB/Bluetooth вернули пользователям.

Android 17: дисплей засыпает вместе с телефоном

С выходом Android 17 (сначала на Pixel и Galaxy S25) весь трюк с виртуальным дисплеем сломался. Google добавил отдельные таймауты сна для групп дисплеев (аконфиг-флаг separate_timeouts), и наш виртуальный дисплей стал попадать в группу 0 туда же, где экран телефона. Гаснет экран телефона под замком, то гаснет и экран ГУ. На 16 эта же группа не выключалась, и всё работало.

Обход пришлось искать заново. Оказалось, что дисплей нужно поднимать не от своего приложения, а от имени shell. После активации лаунчер запускает отдельный процесс через app_process (VdmHostMain, uid 2000), который создаёт VirtualDevice с привязкой companion-устройства (CDM) на com.android.shell и держит дисплей на нём. Такой дисплей в группу 0 не попадает и остаётся RESUMED, даже пока телефон спит под keyguard. Привязка на собственный пакет (ранний вариант) на 17 просто молча не создавалась — работает только через shell.

Подводный камень: раз дисплей принадлежит shell, запуск лаунчера на нём ловит BAL-блок (background activity launch), что лечится разрешением START_ACTIVITIES_FROM_BACKGROUND, которое instrumentation опять же берёт у shell. Всё это закрыто minSdk 37, то есть включается только на Android 17 (ADR 0033, PR #169).

Прикол с тестированием: как фикс под Samsung сломал Pixel

А вот эта история - чистый учебник, почему вендорные костыли нельзя включать глобально.

Новый shell-хост завёлся на Pixel со второй попытки, а на S25 Ultra с 6. На Samsung (SM-S942B и родня) хост убивало сигналом KILL за 1–3 секунды до подключения магнитолы. Причина: при переключении USB в режим головного устройства Samsung перезапускает adbd, а init при этом сносит всю его cgroup вместе с нашим процессом. Лечение нашлось самсунговское: пользователь дополнительно включает «Отладку по USB», а хост после перезапуска просит adbd поднять tcpip:5555 и переподключается через него.

Получается пока я возился с Samsung до 6-ой тестовой сборки, которая ушла в релиз 4.1.0, я не проверил на Pixel.Требование «на Android 17 должна быть включена отладка по USB» и логика перезапуска adbd всё под Samsung. Пользователь с Pixel 7 Pro к тому моменту уже не скачивал новые сборки и не тестировал их. В итоге после выхода 4.1.0 у пикселей всё разом поломалось: проверка «включена ли отладка по USB» на Pixel врала и говорила «выключена», хотя она была включена. Активация не стартовала, и уведомление с кодом сопряжения просто не появлялось. То есть код, написанный, чтобы починить Samsung, сломал Pixel, который до этого прекрасно работал. Хорошо он сам потом протестировал все сборки 2,5,6 и 4.1.0 и указл где работает.

В 4.1.1 пришлось разделить: все самсунговские костыли (требование USB-отладки, перезапуск adbd, tcpip:5555) теперь включаются только под Build.MANUFACTURER == "samsung", а Pixel идёт прежним путём. Урок ровно тот же, что я усвоил ещё в версии 1.1.6 на своей Honda: обход причуды конкретного железа включается только флагом этого железа, и никогда — для всех. Казалось бы, уже наступал на эти грабли. Наступил снова, просто с другой стороны.

Часть 6. Свой CarApp

Зачем

Нарисовать карту 2ГИС на нашем дисплее можно и без всякого хоста - просто запустить телефонное приложение в окне. Но телефонный интерфейс в машине не особо удобен - мелкие кнопки, лишние меню и ноль подсказок на приборке. У 2ГИС и Яндекса есть автомобильные версии на Car App Library (androidx.car.app): карта во весь экран, крупные карточки манёвров, шаблоны, которые рисует хост. Хостом обычно выступает Android Auto. Значит, надо стать хостом самим.

Кстати, именно из-за этого в проекте впервые появился AndroidX: до 4.0 все 27 тысяч строк обходились без него, со своим минимальным protobuf и программными View.

Почему именно хост, а не уведомления

Был и другой путь, его предложил Ильдар: не притворяться хостом, а читать уведомления навигатора. Яндекс и Карты шлют turn-by-turn в системную нотификацию, из неё можно вытащить манёвр и расстояние и пробросить это на приборную панель как NAVI_NEXT_TURN_INFO. Масштаб карты в том подходе правился подменой размера поверхности (трюк с DPI). Я этот код посмотрел, и мне показалось, что как-то это всё неправильно. Поэтому расчехлил Opus 4.8 (секурити ишьюз - сечёте).

Уведомление даёт только текст: «через 200 м направо, улица такая-то». А главное, ради чего люди и держат Я.Навигатор/2ГИС/Гугл.Карты - это ограничение скорости, камеры, спидометр и мини-карта перекрёстка и подсказки навигации на панели приборов, и, у кого-то, в проекции. Ничего этого в нотификации нет и быть не может: эти виджеты рисует сама автомобильная версия приложения. Причем только когда она думает, что работает под настоящим ГУ.

Стать хостом показалось мне единственным способом получить ту самую автомобильную карту целиком: во весь экран, с лимитами и камерами, с родными подсказками, которые заодно уходят и на приборку напрямую из Trip, без всякого чтения чужих нотификаций. А масштаб вместо хрупкой подмены поверхности я сделал перезапуском сессии с новым dpi. От наработок Ильдара при этом никуда не делся фундамент 4.0, виртуальный дисплей и активация через instrumentation из его форка. Заменил только способ доставки навигации!

Как вообще удалось стать хостом для Яндекса

Ещё раз повторюсь: надеюсь если кто-нибудь из команды Яндекс Навигатора прочитает это, то не закроет лазейку. Ведь теперь весь китайский импорт ездит на их картах и говорит с Алисой :)

С 2ГИС всё вышло легко: разбор его APK показал, что createHostValidator() возвращает ALLOW_ALL_HOSTS_VALIDATOR в логе «Accepted - Validator disabled, all hosts allowed». Т.е. пускает кого угодно, а вот Яндекс на том же рукопожатии ответил отказом:

handshake: отказ type=5 IllegalArgumentException:
Unknown host 'com.yegorkapomidorka.headswap', uid:10459

А дальше декомпиляция - самое интересное в этой истории. Клод выдаёт 4 или 5 вариантов, но именно человек решает куда и как идти. Пока что ИИ не может заменить человека в рациональности выбора и правильности направления. А у меня сработала чуйка.

Автомобильный сервис Яндекса (NavigationCarAppService, наследник androidx.car.app.CarAppService) разобрали через apkanalyzer и aapt2. Классы обфусцированы, но логику видно: метод-валидатор строит HostValidator.Builder и делает addAllowedHosts(R.array.hosts_allowlist_sample). То есть никакого allowAll, а конкретный список. Вытащили сам список - это стандартный hosts_allowlist_sample прямо из документации Car App Library: шесть подписей, все шесть гугловские (com.google.android.projection.gearhead и хост автомобильных шаблонов, по три отпечатка). Яндекс просто взял пример из библиотеки как есть, а свою подпись туда добавить забыл.

Ключ нашёлся в методе isValidHost. Он пропускает хост не только по подписи из списка, а ещё в трёх случаях:

  1. Если uid вызывающего совпадает с самим приложением;

  2. если uid равен 1000 (система)

  3. или если у хоста выдано разрешение android.car.permission.TEMPLATE_RENDERER. В логе для этого случая отдельная строка: «Accepted - Host has android.car.permission.TEMPLATE_RENDERER».

Фокус-покус: проверка этого третьего пункта смотрит только на флаг «разрешение выдано» в PackageInfo. Само разрешение android.car.permission.TEMPLATE_RENDERER определяется в Android Automotive, которого на обычном телефоне нет, там его не объявил вообще никто (dumpsys package permission | grep -c TEMPLATE_RENDERER возвращает 0). А раз разрешение свободно, Android позволяет объявить его самому.

И чё вы думаете? Я конечно его добавил в свой манифест:

<permission
    android:name="android.car.permission.TEMPLATE_RENDERER"
    android:protectionLevel="normal" />
<uses-permission android:name="android.car.permission.TEMPLATE_RENDERER" />

Уровень normal - значит система выдаёт разрешение при установке молча, без вопросов пользователю. dumpsys package … TEMPLATE_RENDERER показал granted=true, и на следующем рукопожатии Яндекс сказал «Accepted» и отдал нам карту. В тот момент у меня из уст срывались только матерные слова)))

Лазейку Opus 4.8 нашёл не перебором, а чтением обфусцированного валидатора: заметил, что одна из веток верит голому флагу разрешения, которого на телефоне не существует, а несуществующее разрешение можно объявить за Automotive самому. Чем это грозит? Яндекс может прикрыть одной строкой…

Если Opus 4.8 может такое, то не мудрено, что Антропик всеми силами пытается закрыть использование более продвинутых моделей Opus 5.5 или Fable 5.1 в целях реверс инжиниринга, декомпиляции и всё что связанно с безопаностью.

Протокол

Car App Library - это AIDL поверх обычного bindService. Хост биндится к сервису приложения с действием androidx.car.app.CarAppService, дальше по шагам:

  1. getAppInfo узнаём минимальный и максимальный уровень API приложения;

  2. onHandshakeCompleted с HandshakeInfo договорились об уровне;

  3. onAppCreate → onAppStart → onAppResume, каждый следующий только после ответа на предыдущий;

  4. getManager("app") менеджер, у которого забираем шаблоны;

  5. приложение присылает NavigationTemplate и запрашивает поверхность через ISurfaceCallback.

Сам хост всего лишь 447 строк (carapp/CarAppSession.kt). Шаблоны рендерим своими View: карта на поверхности, поверх неё наши карточки манёвра и полос.

2ГИС: дверь оставили открытой, и пожалуйста не закрывайте

Разбор APK 2ГИС показал, что CarService.createHostValidator() возвращает ALLOW_ALL_HOSTS_VALIDATOR. В логе прямо так и пишется: «Accepted - Validator disabled, all hosts allowed». Сервис экспортирован без разрешения, minCarApiLevel = 4. Подключиться может кто угодно, но дальше начались грабли.

  • Геолокация. 2ГИС поднимает foreground-сервис геолокации, когда к нему цепляется хост. С нашим хостом Android отказывал: «app must be in the eligible state». Т.е. для FGS типа location приложение должно быть на переднем плане или иметь фоновую геолокацию. Своих разрешений и биндов решил не городить - просим пользователя поставить 2ГИС «Разрешать в любом режиме», и всё работает.

  • Скорость, камеры, мини-карта. Долго не появлялись спидометр, лимит скорости и мини-карта перекрёстка, хотя в настоящем AA они есть. Дизассемблер нативной libv4android: MapWidgetSpeedometer и MiniMap::calculatePosition показывают виджеты только при условии visibleArea != stableArea. AA всегда отдаёт эти две области разными (visible где карта не перекрыта карточками, stable минимальная гарантированная), а мы отдавали одинаковые. Стали отдавать visible справа от карточек, а stable ещё меньше и виджеты появились.

  • Масштаб. На 1920×720 карта выглядела как на планшете очень мелко. Поэтому dpi окна наше всё. Поставил потолок 160 dpi и ступени ×0,5, ×⅔, ×⅚. Хитрость: Яндекс считывает dpi только при старте сессии, поэтому смена ступени масштаба перезапускает всю Car App-сессию. На стенде это даёт 21–24 fps на магнитоле, температура корпуса около 35 °C.

Часть 7. Клавиатура, которая уезжает на телефон

Вторичный доверенный дисплей Android по умолчанию получает политику ввода FALLBACK_DISPLAY: клавиатура для него всплывает на экране телефона, а не на магнитоле. Лечится установкой политики LOCAL через IWindowManager.setDisplayImePolicy(id, LOCAL) (право INTERNAL_SYSTEM_WINDOW instrumentation снова берёт у shell), с обязательной проверкой, что система её применила.

Дальше вылезла Gboard. На дисплее 1280×720 при 160 dpi (это 720 dp по короткой стороне). Gboard решает, что перед ней планшет, и делит клавиатуру пополам к краям. Печатать на магнитоле невозможно. Штатная настройка «Разделённая клавиатура» не помогла. Поднял desktop_density: в релизе плотность считается от высоты дисплея, порог около 600 dp, при 720 px ставим 200 dpi, при 480 это 160. Проверено на 200, 213 и 240.

Риски

  • Яндекс может ужесточить валидатор и убрать ветку с TEMPLATE_RENDERER или добавить обязательную проверку подписи хоста. Тогда автомобильный Навигатор у нас откатится в окно телефонного приложения. Ну либо я не найду другой способ))

  • Google в новой версии Car App Library может потребовать подпись и убрать «мягкие» ветки валидатора.

  • Android может перестать выдавать normal-разрешения в чужом namespace android.car.*. Уже есть проблема с виртуальными дисплеями на версии 17: когда гасишь основной экран - экран на ГУ тоже погасает.

Есть и побочный эффект: раз мы объявили это разрешение как normal, нас теперь пустит любое приложение на стандартной Car App Library. А если другое установленное приложение объявит то же разрешение иначе, установка Headswap может упасть на конфликте разрешений.

У 2ГИС, напомню, валидатор просто отключён (allowAll). Это ещё проще закрывается - включить его в любой версии.

И отдельный ограничитель, который лазейкой не обойти: машинная часть Яндекса работает только с активной подпиской Яндекс Плюс. Без неё навигатор показывает заглушку «оформите подписку». Это уже бизнес-правило, а не техника. Будучи продактом более 10 лет, думаю, что Яндекс тоже в выигрыше) ведь кто-то оформит подписку плюс ради этих подсказок на панели приборов и HUD.

Про чужие моды и подписи, и что я НЕ делал

В Telegram-чате проекта ходят моды Яндекс Навигатора, где к автомобильному режиму прикручен обход проверки подписи самого приложения (в народе «YaSign»): патч из MT Manager подменяет PackageInfo.CREATOR рефлексией, чтобы при проверке подписи навигатор видел оригинальный сертификат. Я эти сборки разобрал, чтобы понять, что там внутри, но в Headswap ничего подобного нет и быть не может. Это переподпись чужого приложения, а не наша функция.

Часть 8. Голос и Алиса

По кнопке голосового ассистента запускается «Яндекс с Алисой» (намерение ACTION_ASSIST). Алиса умеет строить маршрут, но отдаёт его диплинком yandexnavi://build_route_on_map?lat_to=...&lon_to=.... Мы перехватываем диплинк и превращаем его в geo:-намерение для нашего же хоста (androidx.car.app.action.NAVIGATE), после чего навигатор строит маршрут уже на экране магнитолы. Маршрут приложение прокладывает на следующем запросе шаблона. Вы только вдумайтесь - Алиса AI по-умолчанию на китайском импорте!

Алиса AI слушает и разговаривает на ГУ

Алиса AI слушает и разговаривает на ГУ

Что дальше

Headswap делает всего один человек с Claude Code. Я перформлю так быстро, насколько это позволяет мне основная работа. Тысячи сообщений в чате, сотни хотелок людей, десятки комитов в день, несколько фичей каждый день…

Дальше только Headswap 5.0! Что это будет? Я пока даже не знаю…

Если у вас машина для Китая с Baidu CarLife и Android-телефон репозиторий, там же ссылки на сайт (временно не работает, привет Beget...) и чат. А если вы сотрудник Яндекса/2ГИС и читаете это, то ну вы поняли, да? Пожалуйста, не надо. Тысячи автовладельцев уже счастливы.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.