@@ -1,4 +1,171 @@
1. [ ] Стартовый экран (тот когда не запущино ничего на вирутальных мониторах) отображается криво, не бинокулдярно. Он тоже должен быть бинокулярным.
2. [ ] Надо добавить возможность проброса звука с виртуального дисплея на телефон. Должен быть переключатель через что выводить звук: через телефон или с очков. то есть надо сделать саму механику проброса, а потом кнопку переключатиль в мобильном приложении
3. [ ] Когда приложение на виртуальном дисплее запущено отображается положение мышки (жолтая точка). Когда она долго не двигается надо её скрывать на очках
4. [ ] Доработать функционал клавиатуры. 1. когда на самих очках поле ввода в фокусе НЕ нужно отображать клавиатуру на очках. 2. когда на телефоне тыкаю на кнопку клавиатуры нужно отобразить клавиатуру, и чтобы все что там вводится вводилось на очках
# 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 <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_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` ).
- Манифест: `<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` уже с `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.
- **Отмашка на начало работ — ждём от пользователя (пока не дана).**