Files

15 KiB
Raw Permalink Blame History

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.
  • Отмашка на начало работ — ждём от пользователя (пока не дана).

Правила ночной сессии (согласовано 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) на постоянку — можно; при равнозначных развилках выбирать самому и документировать.