package pw.binom.rayneovm.glasses import android.accessibilityservice.AccessibilityService import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityNodeInfo /** * Accessibility-сервис GlassesApp. * * Зачем: * - живучесть процесса: система сама поднимает и перезапускает сервис; * - «легальный» запуск Activity из фона; * - ввод текста в поле гостя на VirtualDisplay ([setText], ACTION_SET_TEXT) — IME на * этих очках сменить нельзя (ROM жёстко держит LatinIME), поэтому системная * клавиатура гостя отключается, а текст приходит из телефона сюда. * * Регистрируется в манифесте с конфигом `res/xml/vm_accessibility_service.xml`. * Активируется пользователем вручную: Настройки → Специальные возможности → RayNeo VM. */ class VmAccessibilityService : AccessibilityService() { override fun onServiceConnected() { super.onServiceConnected() instance = this 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?) { // Намеренно пусто: слушаем только чтобы сервис считался живым. } override fun onInterrupt() { // Ничего: сервис не выполняет длительных операций. } override fun onUnbind(intent: Intent?): Boolean { if (instance === this) instance = null return super.onUnbind(intent) } override fun onDestroy() { if (instance === this) instance = null super.onDestroy() } /** Заменяет текст в сфокусированном поле ввода гостя. false — если поля в фокусе нет. */ private fun applyText(text: String): Boolean { val node = findFocusedInputNode() ?: return false return try { val args = Bundle().apply { putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text) } val ok = node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) if (!ok) Log.w(TAG, "ACTION_SET_TEXT вернул false") ok } catch (t: Throwable) { Log.w(TAG, "ACTION_SET_TEXT упал", t) false } } /** * Ищет сфокусированный узел ввода: сначала в активном окне, затем во всех * интерактивных окнах (в т.ч. окно гостя на VirtualDisplay). */ private fun findFocusedInputNode(): AccessibilityNodeInfo? { val active = rootInActiveWindow val activeFocus = active?.findFocus(AccessibilityNodeInfo.FOCUS_INPUT) Log.i( TAG, "findFocused: activeRoot=${active?.packageName} focus=${activeFocus?.className} " + "editable=${activeFocus?.isEditable}", ) if (activeFocus?.isEditable == true) return activeFocus findEditable(active)?.let { Log.i(TAG, "findFocused: редактируемый узел в активном окне: ${it.className}") return it } val ws = windows Log.i(TAG, "findFocused: окон=${ws?.size ?: 0}") ws?.forEach { w -> val root = w.root ?: return@forEach val focus = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT) Log.i( TAG, " окно type=${w.type} pkg=${root.packageName} focus=${focus?.className} editable=${focus?.isEditable}", ) if (focus?.isEditable == true) return focus findEditable(root)?.let { Log.i(TAG, " редактируемый узел: ${it.className} pkg=${root.packageName}") return it } } return null } /** Первый редактируемый (EditText-подобный) потомок в дереве. */ private fun findEditable(root: AccessibilityNodeInfo?): AccessibilityNodeInfo? { root ?: return null if (root.isEditable) return root for (i in 0 until root.childCount) { findEditable(root.getChild(i))?.let { return it } } return null } companion object { private const val TAG = "VmAccessibilitySvc" /** Активный экземпляр сервиса (в том же процессе, что и [VmService]). */ @Volatile private var instance: VmAccessibilityService? = null /** Ввести [text] в текущее поле ввода гостя. false — если поля в фокусе нет. */ fun setText(text: String): Boolean { if (text.isEmpty()) return true return instance?.applyText(text) ?: false } /** Действие для запроса состояния сервиса/проверки активности (диагностика). */ const val ACTION_PING = "pw.binom.rayneovm.glasses.ACCESSIBILITY_PING" } }