15 KiB
1 · Биноокулярный стартовый экран
Стартовый экран (тот когда не запущено ничего на виртуальных мониторах) отображается криво, не бинокулярно. Он тоже должен быть бинокулярным.
Причина: StatusOverlay в app-glasses-vm/.../ui/GlassesScreen.kt — обычный Compose-оверлей,
центрируется по всей ширине 1280px, поэтому попадает на стык глаз и рисуется ОДИН раз, а не в обеих
половинах. Тот же приём уже используется в CursorOverlay (руками: x и x + half).
Решение (согласовано 2026-09-27):
- Перенести в rayneo-mercury удобную view-mate-функцию
Binocularизview-mate/app-glasses/.../ui/ViewUtils.kt(fun Binocular,BinocularSize,BinocularScreenSide,enum Side,ScreenSize,getValue-extension). Она делаетRowиз двухColumn(weight(1f)), каждый прогоняет весьcontentв своей половине, аBinocularScreenSizeсообщает композитору размер одного глаза (W/2 × H). - Использовать
Binocular { ... }только для простых экранов (Starting / NoConnection / NoAppSelected / Error / ShizukuMissing). - НЕ трогать GL-сюрфейс (
BinocularRendererView— он сам бинокулярит на 1280 и кладётся ВНЕBinocular) и НЕ трогатьCursorOverlay— там хитрая отрисовка, оставить как есть.
Где переносить: модуль rayneo-mercury. Кандидаты: :binocular-renderer либо новый маленький
:compose-binocular. Решить при реализации (по зависимостям — Binocular не нужен GL).
2 · Проброс звука с виртуального дисплея на телефон
Добавить возможность проброса звука с виртуального дисплея на телефон. Переключатель: через телефон или с очков. Сначала механика, потом кнопка в мобильном приложении.
Что уже есть (но не доделано):
AudioCaptureService(очки) — НЕ объявлен в манифесте, никто не вызывает;SetAudioModeна очках только логируется (VmService.kt). Сейчас в MIC-fallback, а нуженAudioPlaybackCapture.- Серверного endpoint
/vm-audioНЕТ (WifiServerTransportрегистрирует только/vm). AudioPlaybackManager(телефон) написан, но никем не вызывается;AudioPlaybackService— заглушка.- UI-переключателя нет;
controller.setAudioModeне вызывается.
Решение (согласовано 2026-09-27):
- MediaProjection ОБЯЗАТЕЛЕН для
AudioPlaybackCapture(CAPTURE_AUDIO_OUTPUT— privileged, Shizuku не даёт; Accessibility тоже не даёт). НО диалог можно обойти:MediaProjectionпроверяет AppOpsPROJECT_MEDIA→cmd appops set <pkg> PROJECT_MEDIA allowотключает диалог НАВСЕГДА. Выполнять через Shizuku (один раз, либо при каждом соединении, если appop сбрасывается системой). - ON_PHONE: захват
AudioPlaybackCaptureна очках → стрим на телефон →AudioPlaybackManagerиграет. Обязательно глушить динамик очков (STREAM_MUSIC→ 0 / mute), иначе двойной звук. - ON_GLASSES: стоп захвата, вернуть звук очков.
- Захватывается ВСЁ системное аудио очков, включая звук гостя — так и надо.
- Переключатель — на экране общего статуса очков (
GlassesStatusScreen), не в пульте.
Открытые технические вопросы (проверить на девайсе):
- Работает ли
cmd appops set ... PROJECT_MEDIA allowот shell на RayNeo X3 Pro (SELinux)? - Cбрасывается ли
PROJECT_MEDIAappop при перезапуске/обновлении? Если да — выставлять при каждом соединении. - Глушение:
setStreamVolume(STREAM_MUSIC, 0)глобально; вернуть прежнее значение в ON_GLASSES.
Риск: самая большая задача. Начинать первой.
3 · Автоскрытие курсора
Когда приложение на виртуальном дисплее запущено, отображается положение мышки (жёлтая точка). Когда она долго не двигается — скрывать на очках.
Механики нет вообще. Курсор задаётся в moveCursor (VmGlassesController.kt), скрывается только
в stopGuest.
Решение (согласовано 2026-09-27):
- Watchdog: 3 секунды после последнего действия → скрыть курсор.
- «Движением» считать любое действие с тачпадом: move / click / scroll (
moveCursor/clickCursor/scrollCursor) — все сбрасывают таймер и показывают курсор. - После скрытия при следующем действии курсор появляется (можно на новой позиции — не принципиально).
4 · Клавиатура
- Когда на очках поле ввода в фокусе — НЕ показывать клавиатуру на очках.
- Когда на телефоне тыкаю кнопку клавиатуры — ввести текст, и всё, что вводится, уходит на очки.
Текущее состояние:
- На телефоне есть рабочий
KeyboardDialog, но он не подключён к навигации (ZonaProfile — мёртвый экран). ВRemoteScreen.ktкнопка «Клавиатура» задизейблена. - На очках своей клавиатуры нет. Но когда гости (Zona) получает фокус в поле ввода, он показывает СВОЙ системный IME поверх нашего приложения (один экземпляр на display 0, поэтому не бинокулярный). Это и есть проблема 4.1.
Решение (согласовано 2026-09-27):
- 4.1: написать крошечный IME-заглушку (
VmNoopImeService, 0 UI) внутри GlassesApp и поставить его дефолтным IME навсегда через Shizuku (settings put secure default_input_method <наш IME>). Обратно НЕ возвращать (пользователю не важно). Никакого диалога выбора IME. - 4.2: ввод с телефона — инжект Unicode в
KeyEvent(через Shizuku binder,injectText(String)), чтобы работали русский и английский. Новое сообщениеSetTextв:sharedVmMessage. Кнопка клавиатуры — вRemoteScreen(там сейчас заглушка). - В перспективе:
onAccessibilityEventиз #5 может помочь прятать IME гостя (см. #5).
Открытые вопросы (проверить):
- Пустит ли RayNeo выбрать наш IME дефолтным (
settings put secure default_input_method) через shell? - Нужны ли отдельные кнопки Backspace/Enter на телефонной клавиатуре (обычно да).
5 · Accessibility-сервис (авто-запуск + фоновый процесс) — НЕ ЗАКРЫТА
Рассмотреть возможность добавить сервис в режиме Accessibility: принимать наши команды, делать всё в фоне (mDNS и т.п.), запускать основное Activity, если оно не запущено — легальный авто-запуск.
ЦЕЛЬ (уточнено 2026-09-27): Accessibility должен быть включён ВСЕГДА. В него планируется перенести работу с WiFi и Bluetooth (наши каналы, не Mercury). Так гарантируем, что сервис всегда запущен (система его сама держит/перезапускает, без foreground-нотификации).
Уже сделано (ядро, проверено на девайсе 2026-09-27):
app-glasses-vm/.../VmAccessibilityService.kt— пассивный слушатель: НЕ читает содержимое (canRetrieveWindowContent=false), НЕ инжектит. ВonServiceConnectedподнимаетMainActivity(авто-запуск).res/xml/vm_accessibility_service.xml— конфиг (eventTypes=typeWindowStateChanged).- Манифест:
<service>сBIND_ACCESSIBILITY_SERVICE+ meta-data. - Строки в
res/values/strings.xml. - Проверено: включение работает (и вручную в UI, и программно от shell);
Enabled services+Bound servicesподтверждены; при бинде поднимается процесс иMainActivity(I/VmAccessibilitySvc: Accessibility-сервис подключён→MainActivity запущена из Accessibility-сервиса).
МОДЕЛЬ — КАК У SHIZUKU (согласовано 2026-09-27): provisioning при каждом подключении телефона к очкам проверяет, включён ли Accessibility, и если нет — включает:
settings get secure enabled_accessibility_services
# если нашего сервиса нет:
settings put secure enabled_accessibility_services \
pw.binom.rayneovm.glasses/pw.binom.rayneovm.glasses.VmAccessibilityService
settings put secure accessibility_enabled 1
Выполнять с телефона через уже готовый AdbController (wireless-adb → shell). Проверено: от
shell settings put работает.
ДВА ПУТИ ВКЛЮЧЕНИЯ (делаем оба):
- (A) Основной — с телефона: при коннекте
AccessibilityProvisioner.ensure()(по аналогии сShizukuProvisioner) проверяет/включает через adb. - (B) Самовосстановление — на очках: само приложение при старте проверяет свой сервис (локально, через Shizuku) и включает, если выключен.
ВАЖНЫЕ ОГРАНИЧЕНИЯ (проверено/выяснено):
force-stopснимает accessibility-флаг.am force-stop, обновление APK и т.п. выключают сервис — поэтому provisioning перепроверяет при каждом коннекте (не один раз).- Ребут: надо проверить, переживает ли
enabled_accessibility_servicesперезагрузку очков (обычно secure setting переживает, но проверить на RayNeo). Если нет — задействовать наш HOME-кандидат (MainActivityуже сHOMEintent-filter) и/илиBOOT_COMPLETED. onAccessibilityEvent— мгновенный, никаких тяжёлых операций (ANR → система отрубит сервис). Сетевую логику — в корутины/отдельные потоки.- Перенос WiFi/BLE-логики в сервис: сейчас mDNS + WebSocket server живут в foreground-
VmService. План — перенести host-логику вVmAccessibilityService(или сделать его хостом),VmServiceсделать тонким/убрать. Обсудить при реализации. - Что Accessibility НЕ даёт: не заменяет MediaProjection/PROJECT_MEDIA appop для #2. Для ввода
(в перспективе) может дать
onAccessibilityEvent(фокус окна) для #4. - Отдельно — киоск (Kiosk / Lock Task Mode): требует deviceOwner (
dpm set-device-owner— одноразово, без сброса не снять) либо whitelisting. Проработать позже, отдельной задачей.
Общие согласования
- Расширять
:sharedVmMessageновыми сообщениями (SetText,SetKeyboardActive, реальныйSetAudioModeи т.п.) — ОК (APK всё равно пересобираются). - Все «повышения» делаются с телефона через
AdbControllerпри коннекте (проверено от shell 2026-09-27):- Accessibility:
settings put secure enabled_accessibility_services+accessibility_enabled 1; - MediaProjection без диалога:
cmd appops set pw.binom.rayneovm.glasses PROJECT_MEDIA allow; - дефолтный IME:
settings put secure default_input_method <наш IME>(проверить в #4).
- Accessibility:
- Порядок работы ночью: #2 → #1 → #3 → #4 → #5 (по убыванию риска/размера).
- Коммиты/пуш — по завершении блоков; отдельно rayneo-vm и rayneo-mercury.
- Отмашка на начало работ — ждём от пользователя (пока не дана).
Правила ночной сессии (согласовано 2026-09-27)
- Упёрся в проблему — не стоим. Зафиксировать её в
BLOCKERS.md(корень репо): задача, что сделано, в чём стена, что проверено, варианты обхода. Затем перейти к следующей задаче. - Погасить экран очков в конце. Когда все задачи закрыты ИЛИ дальнейшее продвижение
невозможно — ОБЯЗАТЕЛЬНО выключить экран очков:
adb -s A06B4AB933C4103 shell input keyevent 223(KEYCODE_SLEEP), либо черезScreenControl(Канал 1) / ShizukuSET_SCREEN. Проверить поmWakefulness=Dozing/Asleep. - Все три пре-авторизации выданы: ребут очков — можно; менять системные настройки очков (Accessibility / PROJECT_MEDIA / дефолтный IME) на постоянку — можно; при равнозначных развилках выбирать самому и документировать.