UixService é um aplicativo Android que expõe a árvore de acessibilidade da tela atual e permite enviar comandos de automação direto via shell, sem precisar de Python, servidor externo nem app com interface gráfica.
Ele roda como um AccessibilityService, acompanha a tela ativa e abre um servidor TCP local em:
127.0.0.1:9001(apenas dentro do próprio Android / container)
A partir daí, qualquer processo com acesso ao shell (ex.: container Android, Termux, adb shell, etc.) pode:
- inspecionar a UI atual em formato JSON
- localizar elementos por texto ou
viewId - clicar em elementos
- digitar texto em campos (
EditText) - executar ações globais (HOME, BACK, etc.)
- fazer swipe/scroll
- aguardar elementos aparecerem (wait com timeout)
Tudo isso apenas mandando uma linha de texto por conexão TCP.
Componentes principais:
-
UixAccessibilityService
Serviço de acessibilidade responsável por:- receber eventos de UI
- manter o último
rootInActiveWindowem memória - converter a árvore de
AccessibilityNodeInfopara JSON - executar gestos (clique e swipe)
-
CommandServer
Servidor TCP embutido no app, responsável por:- escutar em
127.0.0.1:9001 - ler uma linha de comando por conexão (
DUMP,CLICK_ID, etc.) - executar a ação correspondente usando o
UixAccessibilityService - responder sempre em JSON, em uma única linha
- escutar em
Fluxo simplificado:
- O serviço é ativado nas Configurações de Acessibilidade.
- A cada mudança de tela,
onAccessibilityEventatualiza o “root” em memória. - Um cliente no shell envia um comando via
nc(netcat) para127.0.0.1:9001. - O
CommandServerinterpreta o comando, consulta a árvore de acessibilidade e devolve JSON.
- Protocolo: TCP
- Host:
127.0.0.1 - Porta:
9001 - Formato: 1 comando por conexão, 1 linha de texto (terminada com
\n), resposta em 1 linha JSON.
Exemplos usando nc (dentro do Android):
# Dump completo da tela atual
printf 'DUMP\n' | nc 127.0.0.1 9001
printf 'DUMP\n' | nc 127.0.0.1 9001 | jq .
# Clicar em um botão pelo texto visível
printf 'CLICK_TEXT Entrar\n' | nc 127.0.0.1 9001
# Digitar em um campo pelo viewId
printf 'SET_TEXT_ID com.example.app:id/username_input usuario@example.com\n' | nc 127.0.0.1 9001Descrição:Retorna a árvore completa de acessibilidade da tela atual.
Resposta (exemplo simplificado):
{
"text": "",
"content_desc": "",
"view_id": "",
"class_name": "android.widget.FrameLayout",
"package_name": "com.example.app",
"clickable": false,
"enabled": true,
"focusable": false,
"checked": false,
"editable": false,
"bounds": { "left": 0, "top": 0, "right": 413, "bottom": 693 },
"children": [ ... ]
}Cada nó contém pelo menos:
text content_desc view_id class_name package_name clickable, enabled, focusable, checked, editable bounds (left, top, right, bottom) children (lista de nós filhos)
Comando:
FIND_TEXT <texto>
Descrição: Procura o primeiro node cujo text ou content_desc contenha (case-insensitive).
Resposta (encontrou):
{
"found": true,
"node": {
"text": "Entrar",
"content_desc": "",
"view_id": "com.example.app:id/login_button",
"class_name": "android.widget.Button",
"package_name": "com.example.app",
"clickable": true,
"enabled": true,
"focusable": true,
"checked": false,
"editable": false,
"bounds": { "left": 151, "top": 348, "right": 262, "bottom": 389 },
"children": []
}
}Resposta (não encontrou):
{"found": false}Comando:
CLICK_TEXT <texto>
Descrição: Procura um node por text/content_desc e envia um clique (gesto) no centro do seu bounding box.
Resposta (sucesso):
{"ok": true, "x": 206, "y": 368}
Resposta (falha):
{"ok": false, "error": "node_not_found"}Comando:
FIND_ID <view_id>
Descrição: Procura um node pelo viewIdResourceName exato (o campo view_id que aparece no DUMP).
Resposta (encontrou):
{
"found": true,
"node": {
"text": "",
"content_desc": "",
"view_id": "com.example.app:id/username_input",
"class_name": "android.widget.EditText",
"package_name": "com.example.app",
"clickable": true,
"enabled": true,
"focusable": true,
"checked": false,
"editable": true,
"bounds": { "left": 16, "top": 127, "right": 397, "bottom": 175 },
"children": []
}
}Resposta (não encontrou):
{"found": false}Comando:
CLICK_ID <view_id>
Descrição: Procura um node pelo view_id e envia um clique no centro.
Resposta (sucesso):
{"ok": true, "x": 206, "y": 368}Resposta (falha):
{"ok": false, "error": "node_not_found"}Comando:
SET_TEXT_ID <view_id> <texto>
Descrição: Define o texto de um campo (EditText) identificado por view_id, usando a ação ACTION_SET_TEXT.
Tudo após o primeiro espaço depois do view_id é considerado parte do .
Exemplo:
printf 'SET_TEXT_ID com.example.app:id/username_input usuario@example.com\n' | nc 127.0.0.1 9001Resposta (sucesso):
{"ok": true}Resposta (falhas):
{"ok": false, "error": "node_not_found"}ou
{"ok": false, "error": "action_failed"}Comando:
GLOBAL <ação>
Ações suportadas:
BACK
HOME
RECENTS
NOTIFICATIONS
QUICK_SETTINGS
Exemplos:
printf 'GLOBAL BACK\n' | nc 127.0.0.1 9001
printf 'GLOBAL HOME\n' | nc 127.0.0.1 9001Resposta (sucesso):
{"ok": true, "action": "BACK"}Resposta (erro):
{"ok": false, "error": "unknown_global_action"}Comando:
SWIPE x1 y1 x2 y2 [durationMs]
x1, y1: coordenadas iniciais
x2, y2: coordenadas finais
durationMs: duração opcional do gesto em milissegundos (default ~300ms)
Exemplo:
printf 'SWIPE 200 600 200 200 300\n' | nc 127.0.0.1 9001Resposta:
{
"ok": true,
"x1": 200,
"y1": 600,
"x2": 200,
"y2": 200,
"duration": 300
}Se os argumentos forem inválidos:
{"ok": false, "error": "invalid_arguments"}Comando:
WAIT_TEXT <texto> <timeoutMs>
Descrição: Fica consultando a árvore de acessibilidade até encontrar ou até estourar .
Exemplo:
printf 'WAIT_TEXT Entrar 10000\n' | nc 127.0.0.1 9001Resposta (encontrou):
{
"ok": true,
"node": { }
}Resposta (timeout):
{"ok": false, "error": "timeout"}Comando:
WAIT_ID <view_id> <timeoutMs>
Descrição: Idêntico ao WAIT_TEXT, mas usando o view_id exato.
Exemplo:
printf 'WAIT_ID com.example.app:id/login_button 15000\n' | nc 127.0.0.1 9001Resposta (encontrou):
{
"ok": true,
"node": { }
}Resposta (timeout):
{"ok": false, "error": "timeout"}Quando um comando não é reconhecido:
{"ok": false, "error": "unknown_command"}Quando os argumentos são inválidos (ex.: SWIPE com poucos parâmetros):
{"ok": false, "error": "invalid_arguments"}Requisitos para build
Para compilar o APK deste projeto, é necessário:
JDK 17 (ou compatível)
Gradle wrapper já incluído no projeto (./gradlew)
versão alvo usada: Gradle 8.7
Android SDK instalado e configurado
sdk.dir definido em local.properties na raiz do projeto, por exemplo:
sdk.dir=/home/seuusuario/android-sdkComponentes do SDK (instalados via sdkmanager ou Android Studio):
platforms;android-34
build-tools;34.0.0 (ou versão equivalente suportada pelo projeto)
platform-tools
Configurações principais do módulo app (no build.gradle do módulo):
compileSdk 34
minSdk 24
targetSdk 34
Dependências principais:
implementation "androidx.core:core-ktx:1.12.0"Plugins usados:
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}Obs.: ajuste versões de SDK / dependências conforme sua stack, se necessário.
Na raiz do projeto:
./gradlew assembleDebugO APK será gerado em:
app/build/outputs/apk/debug/app-debug.apk
Instalação em um device / emulador / container Android:
adb install -r app/build/outputs/apk/debug/app-debug.apkNo Android (já em su): para ativar o servico e dar as permicoes
su
SERVICE="com.chris.uix/.UixAccessibilityService"
settings put secure enabled_accessibility_services "$SERVICE"
settings put secure accessibility_enabled 1