Files

184 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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) на постоянку — можно; при равнозначных развилках
выбирать самому и документировать.