# 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` проверяет AppOps **`PROJECT_MEDIA`** → `cmd appops set 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_MEDIA` appop при перезапуске/обновлении? Если да — выставлять при каждом соединении. - Глушение: `setStreamVolume(STREAM_MUSIC, 0)` глобально; вернуть прежнее значение в ON_GLASSES. **Риск:** самая большая задача. Начинать первой. --- # 3 · Автоскрытие курсора Когда приложение на виртуальном дисплее запущено, отображается положение мышки (жёлтая точка). Когда она долго не двигается — скрывать на очках. **Механики нет вообще.** Курсор задаётся в `moveCursor` (`VmGlassesController.kt`), скрывается только в `stopGuest`. **Решение (согласовано 2026-09-27):** - Watchdog: **3 секунды** после последнего действия → скрыть курсор. - «Движением» считать **любое действие с тачпадом**: move / click / scroll (`moveCursor` / `clickCursor` / `scrollCursor`) — все сбрасывают таймер и показывают курсор. - После скрытия при следующем действии курсор появляется (можно на новой позиции — не принципиально). --- # 4 · Клавиатура 1. Когда на очках поле ввода в фокусе — НЕ показывать клавиатуру на очках. 2. Когда на телефоне тыкаю кнопку клавиатуры — ввести текст, и всё, что вводится, уходит на очки. **Текущее состояние:** - На телефоне есть рабочий `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` в `:shared` `VmMessage`. Кнопка клавиатуры — в `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`). - Манифест: `` с `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` уже с `HOME` intent-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. Проработать позже, отдельной задачей. --- # Общие согласования - Расширять `:shared` `VmMessage` новыми сообщениями (`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). - Порядок работы ночью: **#2 → #1 → #3 → #4 → #5** (по убыванию риска/размера). - Коммиты/пуш — по завершении блоков; отдельно rayneo-vm и rayneo-mercury. - **Отмашка на начало работ — ждём от пользователя (пока не дана).** ### Правила ночной сессии (согласовано 2026-09-27) 1. **Упёрся в проблему — не стоим.** Зафиксировать её в `BLOCKERS.md` (корень репо): задача, что сделано, в чём стена, что проверено, варианты обхода. Затем **перейти к следующей задаче**. 2. **Погасить экран очков в конце.** Когда все задачи закрыты ИЛИ дальнейшее продвижение невозможно — ОБЯЗАТЕЛЬНО выключить экран очков: `adb -s A06B4AB933C4103 shell input keyevent 223` (KEYCODE_SLEEP), либо через `ScreenControl` (Канал 1) / Shizuku `SET_SCREEN`. Проверить по `mWakefulness=Dozing`/`Asleep`. 3. Все три пре-авторизации выданы: ребут очков — можно; менять системные настройки очков (Accessibility / PROJECT_MEDIA / дефолтный IME) на постоянку — можно; при равнозначных развилках выбирать самому и документировать.