135 lines
6.0 KiB
Kotlin
135 lines
6.0 KiB
Kotlin
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"
|
|
}
|
|
}
|