diff --git a/TASKS.md b/TASKS.md index 809ccd4..f0fb5aa 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,4 +1,171 @@ -1. [ ] Стартовый экран (тот когда не запущино ничего на вирутальных мониторах) отображается криво, не бинокулдярно. Он тоже должен быть бинокулярным. -2. [ ] Надо добавить возможность проброса звука с виртуального дисплея на телефон. Должен быть переключатель через что выводить звук: через телефон или с очков. то есть надо сделать саму механику проброса, а потом кнопку переключатиль в мобильном приложении -3. [ ] Когда приложение на виртуальном дисплее запущено отображается положение мышки (жолтая точка). Когда она долго не двигается надо её скрывать на очках -4. [ ] Доработать функционал клавиатуры. 1. когда на самих очках поле ввода в фокусе НЕ нужно отображать клавиатуру на очках. 2. когда на телефоне тыкаю на кнопку клавиатуры нужно отобразить клавиатуру, и чтобы все что там вводится вводилось на очках \ No newline at end of file +# 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. +- **Отмашка на начало работ — ждём от пользователя (пока не дана).** diff --git a/app-glasses-vm/src/main/AndroidManifest.xml b/app-glasses-vm/src/main/AndroidManifest.xml index 7b284e6..6446344 100644 --- a/app-glasses-vm/src/main/AndroidManifest.xml +++ b/app-glasses-vm/src/main/AndroidManifest.xml @@ -79,6 +79,22 @@ android:exported="false" android:foregroundServiceType="mediaPlayback" /> + + + + + + + + +