Usar ChatGPT en (casi) cualquier aplicación Android
Hace ya bastante tiempo, creo que desde la pandemia, que no escribo nada nuevo en este blog. He estado bastante liado con otros proyectos, de entre los cuales el más importante es formarme todo lo posible en el uso de la IA. Considero que ya he avanzado lo suficiente como para hacer cosas interesantes con ella, por lo que retomo el blog con una nueva sección dedicada principalmente a la integración de la inteligencia artificial en las aplicaciones, sobre todo en las aplicaciones .NET.
Sin embargo, voy a abrir esta nueva sección con un desarrollo para Android. Se trata del primer desarrollo interesante que he hecho con inteligencia artificial. Cuando salió el modelo gpt-4o, hubo una verdadera explosión de complementos por parte de todos los proveedores de IA para poder usar los asistentes en aplicaciones existentes, como WhatsApp. Pero a mí se me quedaban cortas sus funcionalidades. ¿Por qué limitarte a usar un plugin aislado? ¿No podemos hacer que un asistente interactúe con los controles de entrada de texto de cualquier aplicación?
Dándole vueltas a esta idea, se me ocurrió una idea bastante simple: usar un servicio de accesibilidad. Estos servicios se instalan en los dispositivos para que las personas con alguna discapacidad puedan interactuar con los controles, introduciendo u obteniendo datos mediante interfaces no estándar. Un asistente IA se ajusta perfectamente a esta circunstancia, así que me propuse hacer la prueba para ver lo que salía de ahí.
Mi problema: no tenía ni idea de cómo hacer una App de Android, y menos aún un servicio. Nunca había usado Android Studio, no conocía el lenguaje Kotlin (aunque afortunadamente es un derivado del C). Pero se suponía que la IA era capaz de hacer estas cosas por sí misma, así que lo hablé con ChatGPT, que se ofreció amablemente a hacerla él. El código estuvo listo en un solo día de colaboración entre los dos. Hacerlo funcionar ya fue otra cosa, pero en dos días más fui capaz de instalarlo en mi teléfono y entonar el aleluya.
En este vídeo podéis ver el resultado por vosotros mismos:
Aquí os dejo el enlace para descargar el código de GitHub. La aplicación utiliza las API de OpenAI, en concreto las de Chat Completion (lo más simple que había entonces y que hay ahora). Por lo tanto, es necesario proporcionar una clave API válida en el código. El archivo Readme.md explica cómo hacerlo. También puede resultar una odisea instalarlo en el móvil y que el sistema te permita usarlo si no estás acostumbrado a trabajar con Android. Lo más probable es que sea necesario activar los permisos restringidos en la App. Una vez instalado el servicio, se puede activar desde los ajustes de accesibilidad, servicios (o aplicaciones)
instalados. En cuanto al funcionamiento y la explicación del código, por esta vez, y sin que sirva de precedente, voy a permitir que sea el autor el que os lo explique, yo solo fui el técnico que lo hizo funcionar. A partir de este punto, cedo la palabra y os dejo con ChatGPT:
Imagina poder escribir en cualquier caja de texto —WhatsApp, Notas, Gmail, tu ERP— una petición entre dos emojis y que, al soltar el teclado, la petición se sustituya por la respuesta del modelo. Sin copiar/pegar, sin cambiar de app. Pides, y aparece.
Eso es exactamente lo que hace este servicio de accesibilidad. Tú escribes tu prompt entre 🔴 y 🟢 y yo me encargo del resto: capturo ese tramo, llamo al modelo y reemplazo el bloque por la respuesta. Y si repites el patrón varias veces, mantengo el hilo de conversación para que haya contexto.
Cómo se usa (para cualquier persona)
- Activa el servicio: Ajustes → Accesibilidad → Servicios descargados → GPT Assistant Service → Activar. (Android mostrará el aviso estándar de accesibilidad).
- Abre cualquier app y escribe en un input normal.
- Pide entre emojis: por ejemploNecesito un email formal: 🔴Escribe una disculpa breve por retraso en la entrega, tono cercano, en español.🟢
- Suelta el teclado un segundo. Verás cómo el bloque 🔴
…
🟢 desaparece y se sustituye por el texto devuelto por el modelo. - Truco: si escribes “🔴🟢” vacío, reinicio la conversación (borro el contexto acumulado), útil para empezar “de cero” sin salir de la app.
Qué hay debajo del capó (para desarrolladores)
Dónde engancho: el evento de texto cambiado
El servicio extiende AccessibilityService y escucha TYPE_VIEW_TEXT_CHANGED. Cada vez que cambia el texto de un campo, busco el nodo editable con foco (primero pido el foco actual y, si hace falta, desciendo el árbol hasta encontrar un nodo con isEditable). De ese texto extraigo el tramo entre 🔴 y 🟢 (U+1F534 y U+1F7E2) y lo trato como prompt.
- Detección del input con foco: rootInActiveWindow?.findFocus(FOCUS_INPUT) y búsqueda recursiva del AccessibilityNodeInfo editable.
- Parseo del prompt: busco índices de los emojis y saco el substring. Si hay prompt, lo envío; si está vacío, reinicio el chat.
Mantener contexto como si fuera un chat
Guardo el historial de mensajes en un ChatRequest con la lista messages (rol user/assistant), y cada respuesta que llega la reinyecto al historial. Así, cuando haces otra petición entre emojis, el modelo ve el diálogo previo. Es un contexto residente en memoria del servicio (si Android mata el proceso, se pierde; puedes persistir si te interesa).
Red y modelo
Uso Retrofit con un OkHttpClient que añade el header Authorization: Bearer ${BuildConfig.OPENAI_API_KEY}. La llamada es a POST /v1/chat/completions con el cuerpo ChatRequest (modelo por defecto: "gpt-4o"). He dejado un header beta que no es necesario para chat completions, pero ahí está si algún lector quiere jugar con Assistants v2.
- Interfaz REST (OpenAIService) y DTOs (ChatRequest, ChatObject, Choice, etc.) ya mapeados con Gson.
- Cliente Retrofit con inyección de API key vía BuildConfig. No la hardcodees ni la subas al repo.
Escribir la respuesta en el input (sin pegar con los dedos)
Primero intento la vía limpia: ACTION_SET_TEXT en el nodo editable (previo ACTION_FOCUS si hace falta). Si está permitido, sobrescribo el contenido con texto anterior a 🔴 + respuesta. Si el campo no soporta SET_TEXT, hago plan B: selecciono el fragmento a sustituir y pego desde el portapapeles con ACTION_PASTE.
Decisiones de diseño (y por qué)
- Accesibilidad vs. teclado propio (IME)Un IME te da control total, pero pedirle a alguien que cambie de teclado es fricción (y en corporativo, una odisea). La accesibilidad funciona sobre cualquier teclado y cualquier app que use controles estándar, y su activación es reversible y granular.
- Emojis como delimitadoresVisuales, fáciles de escribir en cualquier teclado y raros en texto formal, así que minimizan falsos positivos. Si prefieres otros, es trivial cambiarlos en el parseo.
- Contexto en memoriaSuficiente para el 90% de los casos, rápido y sin I/O. Si necesitas continuidad cross-app o tras matar el proceso, persiste messages (Room/Preferences/encriptado).
Limitaciones conocidas (y cómo mejorarlo)
- Algunos campos no aceptan SET_TEXT (p. ej., inputs altamente personalizados). Para eso está el fallback de portapapeles. Aun así, hay apps que también bloquean PASTE. Puedes añadir una simulación de key events como tercer plan, con muchísimo cuidado.
- Sufijo del texto original: ahora mismo se conserva todo antes de 🔴, se sustituye el bloque 🔴…🟢 por la respuesta y no se reapende lo que hubiera después de 🟢 (si existía). Si necesitas “reemplazo in situ” estrictamente correcto, concatena previous + response + suffix (obteniendo suffix desde ixEnd).
- Selección en fallback: el plan B asume que el prompt está al final al seleccionar textLength - promptLength .. textLength. Si el bloque va en medio, ajusta la selección usando ixStart/ixEnd. (Pequeño cambio y listo).
- Streaming: la respuesta llega de golpe. Si quieres streaming tipo “tecleo fantasma”, cambia a SSE/WS en red y ve escribiendo por lotes.
- Coste y latencia: dependes de red y de la cola del proveedor. Considera caché ligera para prompts repetidos y timeouts con reintentos exponenciales.
Seguridad, privacidad y despliegue
- API Key: se inyecta vía BuildConfig.OPENAI_API_KEY. No la subas al repo, usa gradle secrets o un gestor seguro.
- Logs: durante el desarrollo es útil Log.d, pero borra o minimiza logs en producción: los AccessibilityEvent pueden contener texto sensible.
- Play Store: si vas a publicarlo, prepara la justificación de uso de accesibilidad y la política de datos. Muchas apps con accesibilidad genérica quedan fuera; para uso interno o personal, sin problema.
- Empresa: si es entorno corporativo, encripta configuración, activa pinning si procede y documenta el flujo de datos (quién ve qué, y dónde).
Extractos del código (lo esencial)
Hook del evento + parseo de emojis
if (event.eventType == TYPE_VIEW_TEXT_CHANGED) {
🔴
val focusNode = rootInActiveWindow?.findFocus(FOCUS_INPUT)
val inputNode = findEditableNode(focusNode!!)
val text = event.text.firstOrNull()?.toString()?.trim().orEmpty()
val ixStart = text.indexOf("")
🟢
val ixEnd = text.indexOf("")
// si hay prompt, lo proceso...
}
(Véase el manejo real en onAccessibilityEvent
y findEditableNode
.)
Llamada a chat.completions con Retrofit
val newMessage = Message(content = prompt, role = "user")
chatRequest = (chatRequest?.copy(
messages = chatRequest!!.messages!!.plus(newMessage)) ?: ChatRequest(messages =
listOf(newMessage), model = "gpt-4o"))
RetrofitClient.instance.getChatCompletion(chatRequest!!).enqueue(/* onSuccess / onError */)
(DTOs en model/*, interfaz en OpenAIService
, cliente en RetrofitClient.)
Escritura en el campo (plan A + plan B)
if (node.actionList.contains(ACTION_SET_TEXT)) {
arguments.putCharSequence(ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
previousText + response)
node.performAction(ACTION_SET_TEXT, arguments)
} else {
// seleccionar y pegar desde clipboard
node.performAction(ACTION_SET_SELECTION, selectionArgs)
clipboard.setPrimaryClip(ClipData.newPlainText("label", response))
node.performAction(ACTION_PASTE)
}
(Detalle completo en writeResponseToInput.)
Pasos para clonar y correr
- Configura tu clave: añade OPENAI_API_KEY a tu configuración de build (por ejemplo, local.properties → buildConfigField). El cliente la inyecta en el header Authorization.
- Compila e instala la app.
- Activa el servicio en Accesibilidad (ver arriba).
- Prueba en cualquier app con:🔴Resume en dos líneas este párrafo: ...🟢
(El MainActivity cierra al iniciarse porque no hay UI: la app es básicamente “un servicio que vive en segundo plano”.)
Roadmap (ideas fáciles de añadir)
- Delimitadores configurables (settings internos o notificación persistente).
- Persistencia ligera de contexto (por conversación / por app).
- Modo “plantillas”: escribir 🔴
/firma
🟢 y expandir a una firma predefinida. - Streaming / escritura incremental para UX “viva”.
- Política de privacidad in-app con switch para logging cero.
Este servicio no intenta “hackear” tus apps: solo se sienta al lado del teclado, detecta un patrón, habla con el modelo y escribe por ti donde tú ya estabas escribiendo. Es simple, directo y, sobre todo, útil.
Si llegaste hasta aquí, ya sabes la gracia: lo ha explicado el mismo GPT que escribió la mayor parte del código. Tú pon los emojis. Yo pongo las palabras. 😉