glasses: пассивный Accessibility-сервис (авто-запуск + живучесть) + TASKS.md с планом
This commit is contained in:
@@ -1,4 +1,171 @@
|
|||||||
1. [ ] Стартовый экран (тот когда не запущино ничего на вирутальных мониторах) отображается криво, не бинокулдярно. Он тоже должен быть бинокулярным.
|
# 1 · Биноокулярный стартовый экран
|
||||||
2. [ ] Надо добавить возможность проброса звука с виртуального дисплея на телефон. Должен быть переключатель через что выводить звук: через телефон или с очков. то есть надо сделать саму механику проброса, а потом кнопку переключатиль в мобильном приложении
|
|
||||||
3. [ ] Когда приложение на виртуальном дисплее запущено отображается положение мышки (жолтая точка). Когда она долго не двигается надо её скрывать на очках
|
Стартовый экран (тот когда не запущено ничего на виртуальных мониторах) отображается
|
||||||
4. [ ] Доработать функционал клавиатуры. 1. когда на самих очках поле ввода в фокусе НЕ нужно отображать клавиатуру на очках. 2. когда на телефоне тыкаю на кнопку клавиатуры нужно отобразить клавиатуру, и чтобы все что там вводится вводилось на очках
|
криво, не бинокулярно. Он тоже должен быть бинокулярным.
|
||||||
|
|
||||||
|
**Причина:** `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.
|
||||||
|
- **Отмашка на начало работ — ждём от пользователя (пока не дана).**
|
||||||
|
|||||||
@@ -79,6 +79,22 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="mediaPlayback" />
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
|
|
||||||
|
<!-- Пассивный Accessibility-сервис (задача #5): живучесть процесса +
|
||||||
|
легальный запуск Activity из фона. Ничего не читает и не инжектит.
|
||||||
|
Активируется вручную: Настройки → Специальные возможности → RayNeo VM. -->
|
||||||
|
<service
|
||||||
|
android:name=".VmAccessibilityService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accessibilityservice"
|
||||||
|
android:resource="@xml/vm_accessibility_service" />
|
||||||
|
</service>
|
||||||
|
|
||||||
<!-- VmShizukuService НЕ объявляется здесь как <service> — это Shizuku UserService,
|
<!-- VmShizukuService НЕ объявляется здесь как <service> — это Shizuku UserService,
|
||||||
который ServiceStarter создаёт через reflection в отдельном isolated-процессе.
|
который ServiceStarter создаёт через reflection в отдельном isolated-процессе.
|
||||||
Если объявить — Shizuku-сервер не сможет привести класс к IBinder
|
Если объявить — Shizuku-сервер не сможет привести класс к IBinder
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package pw.binom.rayneovm.glasses
|
||||||
|
|
||||||
|
import android.accessibilityservice.AccessibilityService
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.accessibility.AccessibilityEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пассивный Accessibility-сервис GlassesApp.
|
||||||
|
*
|
||||||
|
* Зачем: Accessibility-сервис — единственный «легальный» способ для обычного
|
||||||
|
* приложения (без root/Shizuku/system-app) получить:
|
||||||
|
* - живучесть процесса: систему сама поднимает и перезапускает сервис,
|
||||||
|
* - возможность открывать Activity из фона,
|
||||||
|
* - [onAccessibilityEvent] (в будущем — для отслеживания фокуса ввода и клавиатуры, задача #4).
|
||||||
|
*
|
||||||
|
* ВАЖНО: сервис НИЧЕГО не делает с чужими окнами — не читает содержимое
|
||||||
|
* ([android:canRetrieveWindowContent] = false) и не инжектит события. Это
|
||||||
|
* сознательно: минимум привилегий, минимум риска сломать ввод в гостевых приложениях.
|
||||||
|
*
|
||||||
|
* Регистрируется в манифесте через [VM_ACCESSIBILITY_SERVICE] с конфигом
|
||||||
|
* `res/xml/vm_accessibility_service.xml`. Активируется пользователем вручную:
|
||||||
|
* Настройки → Специальные возможности → RayNeo VM.
|
||||||
|
*/
|
||||||
|
class VmAccessibilityService : AccessibilityService() {
|
||||||
|
|
||||||
|
override fun onServiceConnected() {
|
||||||
|
super.onServiceConnected()
|
||||||
|
Log.i(TAG, "Accessibility-сервис подключён")
|
||||||
|
// Авто-старт: если основное приложение ещё не поднято — поднимаем его.
|
||||||
|
// Это и есть «легальный авто-запуск» из задачи #5.
|
||||||
|
runCatching { startActivity(Intent(this, MainActivity::class.java).apply {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}) }
|
||||||
|
.onSuccess { Log.i(TAG, "MainActivity запущена из Accessibility-сервиса") }
|
||||||
|
.onFailure { Log.w(TAG, "Не удалось запустить MainActivity: ${it.message}") }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
|
||||||
|
// Намеренно пусто. Слушаем только для того, чтобы сервис считался живым.
|
||||||
|
// Тут позже появится логика задачи #4 (фокус поля ввода → скрыть IME гостя).
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterrupt() {
|
||||||
|
// Ничего: сервис не выполняет длительных операций.
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "VmAccessibilitySvc"
|
||||||
|
|
||||||
|
/** Действие для запроса состояния сервиса/проверки активности (диагностика). */
|
||||||
|
const val ACTION_PING = "pw.binom.rayneovm.glasses.ACCESSIBILITY_PING"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">RayNeo VM</string>
|
<string name="app_name">RayNeo VM</string>
|
||||||
|
<string name="vm_accessibility_summary">RayNeo VM</string>
|
||||||
|
<string name="vm_accessibility_description">Фоновый сервис RayNeo VM. Держит приложение работоспособным в фоне и позволяет ему самостоятельно запускаться. Не читает содержимое экрана и не управляет другими приложениями.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Конфиг Accessibility-сервиса RayNeo VM.
|
||||||
|
|
||||||
|
Сервис — пассивный слушатель: он НЕ управляет чужими окнами и НЕ инжектит
|
||||||
|
события. Нужен только как «легальный» живучий процесс, который система
|
||||||
|
поднимает/перезапускает сама, и через который можно открывать Activity из фона.
|
||||||
|
|
||||||
|
Никаких eventTypes/feedbackType не запрашиваем сверх необходимого:
|
||||||
|
eventTypes="typeWindowStateChanged" — минимально нужный, чтобы сервис считался
|
||||||
|
активным и `onAccessibilityEvent` вызывался. canRetrieveWindowContent=false —
|
||||||
|
содержимое окон нам не нужно, меньше привилегий.
|
||||||
|
-->
|
||||||
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:accessibilityEventTypes="typeWindowStateChanged"
|
||||||
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
|
android:accessibilityFlags="flagDefault"
|
||||||
|
android:canRetrieveWindowContent="false"
|
||||||
|
android:description="@string/vm_accessibility_description"
|
||||||
|
android:notificationTimeout="100"
|
||||||
|
android:summary="@string/vm_accessibility_summary" />
|
||||||
Reference in New Issue
Block a user