glasses: пассивный Accessibility-сервис (авто-запуск + живучесть) + TASKS.md с планом

This commit is contained in:
2026-09-27 05:46:14 +03:00
parent 9914488258
commit 2125788e25
5 changed files with 264 additions and 4 deletions
@@ -79,6 +79,22 @@
android:exported="false"
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,
который ServiceStarter создаёт через reflection в отдельном isolated-процессе.
Если объявить — 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>
<string name="app_name">RayNeo VM</string>
<string name="vm_accessibility_summary">RayNeo VM</string>
<string name="vm_accessibility_description">Фоновый сервис RayNeo VM. Держит приложение работоспособным в фоне и позволяет ему самостоятельно запускаться. Не читает содержимое экрана и не управляет другими приложениями.</string>
</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" />