Apps inteligentes

Integra tu app en el sistema de inteligencia de Android

Bocinas

Acerca de

Acerca de la charla

Android está evolucionando hacia un sistema inteligente en el que agentes como Gemini organizan tareas en todas las apps. Para desbloquear nuevas experiencias del usuario y participar en estos flujos de trabajo de agentes, tu app debe exponer sus capacidades principales.

Esta sesión es una guía práctica para integrar tu app con la capa de inteligencia de Android. Analizaremos las rutas de integración clave, como el MCP de Android integrado en el dispositivo, el MCP del servidor y la automatización de tareas, y proporcionaremos un marco de toma de decisiones para elegir la ruta adecuada para tu caso de uso. También exploraremos las experiencias proactivas con Contexto personal.

Aprenderás a hacer lo siguiente:

  • Selecciona la ruta de integración óptima para las necesidades de tu app.
  • Implementar funciones de inteligencia reactiva y proactiva
  • Equilibrar el rendimiento y la seguridad en las integraciones de agentes

Resumen

  • El MCP de Android es un framework unificado de llamadas a herramientas integrado en el dispositivo que funciona con la biblioteca de AppFunctions de Jetpack. Asigna primitivas de MCP a construcciones de Kotlin y se ejecuta en la IPC de Binder de Android.
  • Las herramientas integradas en el dispositivo se destacan cuando acceden a datos encriptados o sensores del dispositivo. Como alternativa, la automatización de tareas controlada por la plataforma permite que los agentes precargados simulen eventos táctiles sin cambios en el código.
  • Tu app actúa como un servidor de MCP de Android mientras los agentes descubren y llaman a las herramientas. Declara las herramientas con AppFunctionDeclaration, proporciona descripciones de KDoc y evita depender de los valores predeterminados de Kotlin.
  • La plataforma de apps conectadas, que actualmente se encuentra en versión preliminar, te permite probar tus AppFunctions con Gemini. Configura tu app conectada en Google Connected Console y, luego, sube el paquete en Play Console.

Recursos

Gemini Notebook

Temas

  • android-mcp
  • appfunctions
  • gemini
  • apps-conectadas
  • model-context-protocol
  • on-device-ai

Transcripción

Descargar transcripción

Introducción a la era de la inteligencia

  • Bien.

  • Hola a todos.

  • Soy Dan Galpin, de Relaciones con Desarrolladores.

  • Y yo soy Sarah Shamsie, gerente de producto que trabaja en los frameworks de inteligencia para desarrolladores de Android.

  • Nos entusiasma mucho darte la bienvenida a la sesión de hoy.

  • Estamos presenciando un cambio de paradigma en la forma en que los usuarios interactúan con sus dispositivos.

  • En la era de la Web, los usuarios solían comenzar sus recorridos navegando al navegador web y, luego, usando la función de búsqueda.

  • Luego, en la era de los dispositivos móviles, vimos que el comportamiento de los usuarios cambió y comenzaron a usar aplicaciones nativas más especializadas para completar acciones únicas.

  • A continuación, se muestra un ejemplo.

  • Recibo un mensaje de un amigo que me invita a cenar.

  • Para responder, tengo que revisar mi calendario para ver si estoy libre, agregar el evento, reservar un viaje si necesito transporte y, luego, enviarle un mensaje a mi amigo para decirle que puedo ir.

  • Antes, esto requería lo que se conoce como "app-hopping", en el que básicamente tenía que alternar entre diferentes apps en mi dispositivo para completar las acciones y cumplir con mi recorrido.

  • Pero ahora estamos en la era de la inteligencia.

  • En la era de la inteligencia, los usuarios comienzan sus recorridos expresando intenciones y objetivos de alto nivel, en lugar de acciones específicas que necesitan que se realicen.

  • Hay un cambio fundamental en el que ven su dispositivo como algo que anticipa sus necesidades de forma proactiva y los ayuda a cumplir sus objetivos, en lugar de algo que solo habilita acciones.

  • Y notamos que esperan que este framework de inteligencia funcione no solo en sus dispositivos móviles, sino también en un ecosistema de factores de forma enriquecido, que incluye relojes, autos, TVs, laptops, RE y lentes.

  • En la era de la inteligencia, una sola instrucción entre apps puede facilitar el ejemplo de la invitación a cenar que vimos antes.

  • Con solo una instrucción, un agente en mi dispositivo puede ayudarme a habilitar todas las acciones que necesito, desde consultar mi calendario, agregar el evento, reservar el viaje y enviar un mensaje, todo de una sola vez.

  • Hoy, compartiremos más información sobre los planes de Android en este espacio y cómo ustedes, como desarrolladores, pueden crear experiencias de primer nivel.

  • Dan.

  • Gracias, Sarah.

  • Por lo tanto, Android sigue agregando inteligencia en su núcleo, y eso ofrece nuevas formas para que tus apps, ya sabes, se preparen para esta era de la inteligencia.

  • Y el primero de estos dos pilares de inteligencia para desarrolladores es la inteligencia integrada en la app, que, obviamente, se trata de hacer que tus apps sean más inteligentes, de incorporar inteligencia en tu app, y puedes usar modelos integrados en el dispositivo, como Gemini Nano 4, a través de ML Kit para resumir hilos de mensajes, reescribir respuestas y comprender imágenes, o usar lenguaje natural para llamar a las funciones de tu app.

  • Incluso puedes usar modelos de Gemini en la nube directamente en tu app a través de Firebase AI Logic.

  • Pero hoy nos enfocaremos en el segundo pilar: asegurarnos de que las funciones de tu app estén disponibles en el sistema de inteligencia.

  • Entonces, ¿qué es un sistema de inteligencia?

  • Por lo tanto, además de, ya sabes, hablar y alojar esas experiencias de instrucciones de las que Sarah habló antes, es un sistema que permite integraciones profundas para mejorar las experiencias en todo el ecosistema de apps.

  • Por lo tanto, los dispositivos diseñados para la inteligencia de Gemini, por ejemplo, aprovechan estos puntos de integración para proporcionar sugerencias, notificaciones resumidas, entrada de voz mejorada y llenado automático de formularios.

  • Ahora, las sugerencias forman parte de una función llamada asistencia proactiva.

  • Hasta ahora, hemos hablado de la asistencia con instrucciones.

  • Todos estamos acostumbrados a eso, donde un usuario le dice a un agente que realice una o más tareas en su nombre.

  • Sin embargo, la asistencia proactiva se trata de anticipar la próxima acción o acciones que un usuario podría realizar.

  • Por lo tanto, volviendo a nuestro ejemplo de la cena, el agente podría mostrarle al usuario sugerencias de forma proactiva en respuesta al mensaje, como agregar la invitación a su calendario o reservar un viaje compartido.

  • Pero ¿cómo le solicita exactamente al usuario?

  • La asistencia proactiva puede mostrar acciones en lugares como la pantalla de bloqueo, las notificaciones, los teclados virtuales y hasta en apps seleccionadas.

  • Entonces, ¿cómo puedes hacer que tu app aparezca en estos chips proactivos para atraer tráfico?

  • Bueno, ya estás haciendo cosas como compilar con la versión más reciente del SDK y con nuestras herramientas más modernas, pero lo mejor que puedes hacer ahora es asegurarte de que tu app también esté lista para la asistencia con instrucciones, de la que Sarah hablará a continuación.

Asistencia con instrucciones y Android MCP

  • Gracias, Dan.

  • La asistencia con instrucciones implica que los usuarios se dirijan directamente a un agente y le pidan que inicie y organice tareas, incluso en varias apps.

  • Esto puede automatizar flujos de trabajo que antes eran manuales y consumían mucho tiempo.

  • En el caso de nuestro sistema de inteligencia en Android, nos enfocamos principalmente en agentes del sistema como Gemini, que pueden acceder a las apps en el dispositivo del usuario.

  • Hay algunas formas diferentes en que tu app puede aparecer en esta experiencia y en que puede interactuar con el agente del sistema.

  • Está nuestra solución de automatización de tareas impulsada por la plataforma y, luego, las integraciones directas de la API en el agente del sistema.

  • Siempre recomendamos compilar con integraciones directas de la API, siempre que sea posible.

  • Los desarrolladores controlan estos elementos, por lo que puedes decidir y controlar cómo se muestra tu app y cómo se muestra tu marca.

  • También son más rápidos y confiables, lo que ofrece una experiencia más premium a tus usuarios.

  • Existen dos rutas de API que puedes considerar cuando quieras integrarte con el agente.

  • El Protocolo de contexto del modelo, o MCP, es la práctica recomendada de la industria para las llamadas a funciones del servidor.

  • Sin embargo, para las llamadas a herramientas integradas en el dispositivo, anunciamos formalmente Android MCP, el framework unificado de llamadas a funciones en Android.

  • Android MCP cuenta con la tecnología de nuestra biblioteca de AppFunctions de Jetpack, que se alinea estrechamente, tanto técnica como conceptualmente, con MCP, a la vez que admite una implementación de Kotlin idiomática y familiar con la que muchos desarrolladores de Android están familiarizados.

  • Esto hace que la implementación de la API sea relativamente intuitiva y fácil de usar, y puedes aprovechar cualquier aprendizaje que hayas obtenido de una integración de MCP del servidor ya establecida.

  • Muchas primitivas de MCP se asignan de forma unívoca a una construcción de AppFunctions de Jetpack.

  • Veamos algunos ejemplos para ilustrar esto.

  • Un extremo de herramienta de MCP se asigna correctamente a una función de Kotlin anotada con AppFunctionDeclaration.

  • Tus modelos de pydantic se asignan a clases de datos de Kotlin anotadas con AppFunctionSerializable.

  • Sin embargo, en lugar de HTTP transmitible o transporte de E/S estándar, el MCP de Android se ejecuta en la IPC de Binder de Android optimizada.

  • Hoy, nos complace anunciar que la biblioteca de Jetpack, AppFunctions, ya está en versión beta.

Casos de uso clave para MCP de Android

  • Bien, seguro te estás preguntando cuándo tiene sentido usar una ruta de API y cuándo tiene sentido usar Android MCP, específicamente.

  • Como principio general, el MCP de Android es especialmente útil cuando quieres aprovechar el contexto local o integrado en el dispositivo para completar el recorrido del usuario.

  • Analicemos algunos ejemplos en los que el MCP de Android realmente se destaca.

  • En particular, deberías considerar el MCP de Android cuando necesites acceder a datos locales encriptados.

  • Esto es especialmente importante para las apps de mensajería y comunicación, en las que el historial de mensajes debe permanecer integrado en el dispositivo debido a la encriptación de extremo a extremo.

  • Android MCP también es útil para acciones puramente integradas en el dispositivo y sin conexión, en las que los agentes admiten esto.

  • Nuestro socio de Wear OS, Samsung Health, lanzó una función para iniciar una carrera y comenzar ejercicios, que se orquestan por completo integrado en el dispositivo.

  • Los usuarios suelen querer que este tipo de acciones se realicen con mucha rapidez, alta confiabilidad, baja latencia y, a veces, incluso en entornos con poca conectividad. Asegurarse de que toda la llamada a la herramienta, el enrutamiento y el enrutamiento de consultas puedan realizarse integrado en el dispositivo garantiza esto.

  • Android MCP también es la ruta de API preferida si quieres acceder a los sensores del dispositivo o interactuar directamente con su hardware.

  • Por ejemplo, muchas apps de automóviles quieren permitir que los usuarios destraben las puertas de sus vehículos de forma instantánea a través de la conectividad de UWB y, a menudo, con manos libres.

  • Por lo tanto, Android MCP puede ayudar con eso.

  • Por último, es tu solución si no hay un extremo de nube disponible para tus apps o API.

  • Muchas de las apps propias de nuestros fabricantes de dispositivos son solo del cliente, por lo que usan MCP de Android por este motivo.

  • Por ejemplo, se puede usar Gemini para buscar y recuperar fotos de apps de galería que solo se ejecutan en el cliente, como la Galería de Xiaomi.

  • Aquí, puedo abrir Gemini y pedirle que busque las fotos del robot de Android que tomé la semana pasada en la Galería de Xiaomi.

  • Xiaomi expuso una función de la app para buscar fotos, a la que Gemini llama, y las fotos relevantes se devuelven y se muestran directamente en la IU de la app de Gemini, por lo que el usuario nunca tiene que salir.

  • Y confiamos en la misma arquitectura para las acciones integradas en el dispositivo en el SO Android.

  • Android MCP potencia la función Ayuda del dispositivo que se lanzó recientemente, que es una integración directa del SO Android en la app de Gemini.

  • Esta función ya se lanzó en dispositivos Pixel y Lenovo, y actualmente se está escalando horizontalmente en el ecosistema más amplio de OEM de Android.

  • Los usuarios pueden usar la Ayuda del dispositivo accediendo a Gemini y preguntando cómo pueden optimizar su configuración, la configuración del dispositivo y la configuración del sistema para mejorar su experiencia con el dispositivo.

  • Por ejemplo, en lugar de buscar en varios menús, un usuario daltónico puede preguntarle a Gemini: "Soy daltónico.

  • ¿Cómo puedo hacer que este teléfono se adapte mejor a mis necesidades?"

  • Gemini llamará e invocará funciones de la app local, razonará de forma inteligente sobre la solicitud y sugerirá formas reales en las que el usuario puede cambiar la configuración del sistema y formas específicas en las que puede cambiar la configuración del dispositivo para mejorar la experiencia.

  • Luego, los controles integrados en la app se muestran directamente en la IU de la app de Gemini, para que los usuarios puedan tomar el control de inmediato.

  • Por ejemplo, para cumplir con esta solicitud de optimización para personas con daltonismo, Gemini mostró este botón de activación de corrección de colores, de modo que los usuarios puedan realizar el cambio sobre la marcha.

  • Las experiencias de MCP de Android ya están disponibles en dispositivos seleccionados, y estamos trabajando con varios socios para incorporar aún más casos de uso al ecosistema Android.

  • Si bien las declaraciones explícitas de herramientas de MCP de Android son nuestro estándar de excelencia, sabemos que habrá casos en los que los usuarios les pedirán a los agentes que completen casos de uso complejos o menos comunes, y aquí es donde entra en juego nuestra función de automatización de tareas impulsada por la plataforma.

  • En este modelo, el agente precargado del OEM puede usar las APIs de control de computadoras de Android para simular eventos táctiles en la superficie de una app.

  • Esto no requiere cambios en el código por parte de los desarrolladores y requiere que el agente presione la IU como lo haría un usuario, analizando la pantalla de la app.

  • Para garantizar el control y la seguridad de los usuarios, es fundamental que los agentes se detengan en acciones potencialmente sensibles, como los flujos de pago, y soliciten la confirmación del usuario para continuar.

  • Veamos un ejemplo en acción.

  • Aquí, un usuario tomó una foto de un tour de café y chocolate, y quiere pedirle a Expedia o a Gemini que busque en Expedia y vea si puede encontrar un tour similar, pero ajustando la cantidad de personas a seis asistentes.

  • Sin necesidad de una integración directa de la API, Gemini navega por la IU de Expedia en segundo plano de la misma manera que lo haría un usuario.

  • Luego, muestra las opciones relevantes que encontró y se las entrega al usuario sin problemas cuando llega el momento de la confirmación y la reserva finales.

  • Es importante tener en cuenta que estas tres rutas diferentes no son mutuamente excluyentes y, de hecho, funcionan mejor juntas.

  • Los usuarios y desarrolladores deben esperar que los agentes puedan coordinar herramientas en las implementaciones de servidor y, luego, también en la app para Android local para ofrecer la experiencia más completa y holística a los usuarios.

Análisis técnico detallado de MCP de Android

  • Bien.

  • Gracias, Sarah.

  • Analizaremos en mayor profundidad el MCP de Android.

  • Y, cuando sea posible, quiero que sepas que estoy extrayendo los fragmentos de código de MCP de Jetpacker, que es nuestra app de ejemplo de viajes, que, de hecho, se creó con la ayuda de Antigravity.

  • Ya viste esto en la presentación.

  • Está repleta de funciones inteligentes geniales, como integraciones integradas en el dispositivo con las APIs de IA generativa y reconocimiento de voz de ML Kit.

  • Ahora, el MCP de Android y el MCP basado en la nube tienen una estructura similar.

  • Ambos tienen clientes y servidores.

  • En el mundo del MCP, los servidores proporcionan herramientas, y los clientes pueden descubrirlas y llamarlas.

  • Ahora, con Android MCP, tu app es el servidor y el agente es el cliente.

  • La plataforma de Android desempeña el papel de registro de herramientas.

  • Ahora, las apps comienzan registrando un servicio para proporcionar el servidor, y nosotros comenzaremos agregando las dependencias de la biblioteca de Jetpack de AppFunctions en nuestro ejemplo aquí.

    implementation(libs.androidx.appfunctions)
    ksp(libs.androidx.appfunctions.compiler)
  • Luego, usaremos anotaciones de KSP para ayudar a crear un AppFunctionService.

  • Ahora, este servicio significa que las funciones de tu app pueden respetar correctamente los ciclos de vida de Android.

  • Podemos usar la inserción de dependencias con Hilt para insertar repositorios de datos que nuestras AppFunctions puedan usar.

    @RequiresApi(36)
    @AndroidEntryPoint
    @AppFunctionServiceEntryPoint(
        serviceName = "JetPackerAppFunctionService",
        appFunctionXmlFileName = "jetpacker_app_function_service"
    )
    abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
        @Inject internal lateinit var tripDao: TripDao
        @Inject internal lateinit var expenseDao: ExpenseDao
        ...
  • Lo siento.

  • Volvamos a nuestro diagrama.

  • Después de configurar el servicio para nuestro servidor de MCP para Android, la app registra las herramientas en forma de AppFunctions, junto con los modelos de datos asociados que usan.

  • Ahora puedes registrar un modelo de datos con la anotación AppFunctionSerializable, y este KDoc es bastante genial.

    /** Represents an event in a trip itinerary. */
    @AppFunctionSerializable(isDescribedByKDoc = true)
    data class TimelineEventSerializable(
      /** The event's unique identifier. */
      val id: String,
      /** The trip ID this event belongs to. */
      val tripId: String,
      /** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
      val type: String,
      /** The timestamp of the event. */
      val timestamp: Long,
      /** The title of the event. */
      val title: String,
      /** The location of the event. */
      val location: String,
      /** A description of the event. */
      val description: String?,
      /** The title of the trip this event belongs to. */
      val tripTitle: String? = null,
      /** Optional title of a matched itinerary event when added as a reminder. */
      val matchedEventTitle: String? = null,
      /** Execution status of the tool call (e.g., SUCCESS). */
      val status: String? = "SUCCESS",
      /** Human-readable completion message instructing the agent not to repeat the call. */
      val message: String? = null,
    )
  • Solo se convertirá en metadatos que el agente usará para comprender cómo usar el modelo.

  • Puedes usar la anotación AppFunctionDeclaration para registrar tu función como una herramienta.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
        val events =
          if (dbEvents.isNotEmpty()) { dbEvents } else {
            val fallback = DummyData.events.filter { it.tripId == targetTripId }
            if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
            fallback
          }
        events.map { it.toSerializable(tripTitle = trip?.title) }
      }
    }
  • Y con isDescribedByKDoc establecido en verdadero, KDoc le proporciona al agente una descripción para comprender lo que hace tu función, y también ayuda a los humanos a entenderla.

    /**
     * Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
     * trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
     * specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
     * visitor tips.
     *
     * @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    ...
  • Estas son algunas prácticas recomendadas para el diseño de parámetros.

    /**
     * @param tripId Optional ID of the trip (e.g., '2026-1').
     *   Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        // resolveTrip defaults to the active trip if tripId == null
        val trip = resolveTrip(tripId = tripId)
  • En primer lugar, debes enfocarte en el subconjunto mínimo de parámetros con nombres claros y distintos.

  • Haz que los parámetros no esenciales sean anulables y resuelve sus valores predeterminados en el cuerpo de la función.

  • Esta no es solo una práctica recomendada.

  • No funcionará, ya que se ignorarán los valores predeterminados de Kotlin porque la biblioteca de AppFunctions pasa un valor nulo para los parámetros anulables omitidos.

  • Ni siquiera voy a mostrar un ejemplo de este primer caso, pero no uses parámetros comodín.

  • Aspectos como el contexto completo o el historial de conversaciones son simplemente malos.

  • Pueden hacer que los agentes pasen grandes cantidades de datos y que se produzca un robo de datos, lo que es una mala práctica.

  • Dado que la AppFunction se ejecuta de forma predeterminada en el subproceso de IU, debes asegurarte de que cualquier operación de larga duración se realice en segundo plano eligiendo el despachador de corrutinas adecuado.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
        tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
        val events =
          if (dbEvents.isNotEmpty()) { dbEvents } else {
            val fallback = DummyData.events.filter { it.tripId == targetTripId }
            ...
  • Te recomendamos que arrojes excepciones de AppFunction cuando sea necesario con información detallada para que el agente pueda comprender cómo resolver un error.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun send(
        message: Message
    ): Result {
      ...
      throw AppFunctionException(
        errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
        errorMessage = "To address has invalid syntax!"
      )
    }
  • Muy bien, ahora que tenemos una AppFunction, volvamos a nuestro diagrama de arquitectura.

  • La app cliente del agente se registra en la plataforma para llamar a AppFunctions.

  • Por ejemplo, Gemini es un modelo de este tipo.

  • Para ello, deben tener un permiso privilegiado, y los agentes registrados pueden buscar metadatos de AppFunction en la plataforma de Android y decidir si deben ejecutar una o más AppFunctions registradas para completar la solicitud de un usuario.

  • Ahora, hablando de una o más AppFunctions, ¿cómo decide el agente?

  • Bueno, lo último que debes hacer es describir las capacidades de tu app en un archivo de metadatos de la app.

    <AppFunctionAppMetadata
    xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
    
      appfn:description="This app manages trips and travel expenses.
    
      Operational Patterns:
        - Use 'getAllTrips' to find the correct 'tripId' before
          adding expenses or managing trip details.
    
      Constraints:
        - Expenses must be associated with a valid trip ID."
    
      appfn:displayDescription="@string/user_visible_description" />
  • Y los agentes seleccionan las herramientas leyendo primero esta descripción dentro de tu AppFunctionAppMetadata y, luego, leyendo las descripciones con las AppFunctions individuales.

  • Por lo tanto, debes enfocar el comienzo de la descripción con una breve explicación del dominio principal de tu app o herramientas.

  • Y puedes seguir esto con patrones operativos para ayudar al agente a conocer el flujo lógico de las operaciones, lo que es bueno para describir los requisitos previos o dirigir al agente hacia las rutas óptimas para completar la tarea.

  • También puedes agregar una sección sobre restricciones que defina los límites lógicos de los tipos de datos de entrada o refuerce los patrones operativos.

  • Esos son los conceptos básicos del MCP de Android, pero hay algunas cosas más que puedes hacer.

  • Una de las cosas que puedes hacer, que es bastante genial, es habilitar e inhabilitar AppFunctions de forma dinámica.

    viewModelScope.launch {
        val newState =
            if (enabled) {
                AppFunctionManager.APP_FUNCTION_STATE_ENABLED
            } else {
                AppFunctionManager.APP_FUNCTION_STATE_DISABLED
            }
        appFunctionManager?.setAppFunctionEnabled(id, newState)
    }
  • Esto es importante, por ejemplo, si quieres que AppFunction solo esté disponible si el usuario está conectado.

  • Otra cosa que puedes hacer es crear AppFunctions que tengan como alcance una pantalla o actividad específica de tu app.

    @AppFunctionSignature(
        scope = AppFunctionMetadata.SCOPE_ACTIVITY,
        appFunctionXmlFileName = "cart_functions"
    )
    fun interface AddCurrentItemToCart {
        /** Adds the item currently shown on screen to the cart. Returns the new cart size. */
        suspend fun addToCart(quantity: Int): Int
    }
  • Por lo tanto, en lugar de declarar la AppFunction, declaras AppFunctionSignature.

  • Luego, puedes implementar un controlador para la AppFunction en tu IU.

    val addToCartAppFunctionAdapter =
        appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
    
    coroutineScope.launch {
        appFunctionManager.handleAppFunction(
            addToCartAppFunctionAdapter.adapt { quantity ->
                val newCartSize = cart.addItem(quantity)
                newCartSize
            }
        )
    }
  • Y lo mejor de todo es que el ciclo de vida de los controladores se vincula automáticamente al ciclo de vida del alcance de la corrutina que usas.

  • Ahora, querrás almacenar en caché este adaptador de solicitudes de AppFunctions, perdón, el adaptador de solicitudes de AppFunctions de control, ya que la biblioteca usa la reflexión de forma interna para crear una instancia de él.

  • También hay funciones para crear AppFunctions con alcance cuyo registro no está vinculado directamente al ciclo de vida de Android.

  • Ahora, un último detalle es que tenemos este elemento llamado nivel de acceso.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
      ...
  • Y puedes obtener y usar el nivel de acceso de self, system o trusted, y esto indica qué funciones son solo para usar en tu propio agente integrado en la app, como uno creado con la IA generativa de ML Kit, qué funciones están disponibles solo para el sistema y qué funciones están disponibles para los agentes del sistema de confianza.

  • Y para ayudarte a crear tu propio agente integrado en la app, como ya anunciamos, viste que ML Kit admite de forma nativa las AppFunctions como parte de su framework unificado de llamadas a herramientas.

  • Las defines una vez y obtienes cobertura tanto en los agentes a nivel del sistema como en los modelos locales integrados en la app, lo que te permite evitar declaraciones de herramientas fragmentadas.

  • Para obtener más información, puedes consultar esta charla, Unlock agentic capabilities in your app with ML Kit GenAI, o simplemente puedes ir y ver las novedades de Jetpack.

  • Es genial.

  • Bien, volvamos con Sara.

  • Bien.

  • Dan acaba de mostrarte cómo funciona MCP de Android de forma interna.

  • Pongámosla en práctica.

  • Recuerda que tu app es un servidor y los agentes son el cliente, por lo que el objetivo debe ser asegurarte de que tu app sea un buen servidor para ellos.

  • Seguiremos a Jetpacker, nuestra app de ejemplo de viajes, desde la primera declaración de herramienta hasta una llamada a función real a través de Gemini.

  • Puedes escribir tus AppFunctions a mano o usar un agente de programación para que te ayude a comenzar.

  • Lanzamos nuestra habilidad de AppFunctions, que puede ayudarte a comenzar a escribir tus funciones, que puedes conectar a Android Studio a través de la Android CLI o usar con el agente de programación que elijas.

    android skills add appfunctions
  • La habilidad funciona en tres pasos.

  • Primero, analiza tu base de código para identificar los recorridos del usuario principales que serían herramientas útiles, como reproducir contenido multimedia, crear una nota o enviar un mensaje.

  • Luego, genera declaraciones de AppFunction que exponen esos recorridos con la implementación y la compatibilidad de la plataforma que necesitan.

  • Por ejemplo, Gradle y KSP, el XML de metadatos de la app y la inserción de dependencias.

  • Por último, perfeccionará tus descripciones de KDoc, ya que esto es exactamente lo que usa el agente para decidir cuándo llamar a tu función y cómo hacerlo.

  • Este es el tipo de resultado que buscamos.

  • Esta es una función addExpense para Jetpacker.

    /**
     * Adds an expense to a trip.
     *
     * @param tripId ID of the trip. If null, the active trip is used.
     * @param category Expense category. If null, it's inferred from the title.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun addExpense(
        title: String,
        amount: Double,
        currency: String,
        tripId: String? = null,
        category: String? = null,
    ): ExpenseSerializable = withContext(Dispatchers.IO) {
        if (amount <= 0.0) {
            throw AppFunctionInvalidArgumentException("amount must be greater than zero")
        }
        val trip = resolveTrip(tripId)
            ?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
        val expense = Expense(trip.id, title, amount, currency,
            category = category ?: inferExpenseCategory(title))
        expenseDao.insertExpense(expense)
        expense.toSerializable(remainingBudgetUsd(trip.id))
    }
  • La descripción es una oración de una línea que comienza con un verbo y un recurso: agrega un gasto a un viaje.

  • Los campos de viaje y categoría admiten valores nulos porque es posible que los usuarios no siempre los especifiquen, pero la función los resolverá en el cuerpo.

  • Cuando una solicitud no se puede completar, arroja un AppFunctionException para que el agente sepa qué salió mal.

Prueba las funciones y los agentes de la app

  • Bien, ahora veamos las pruebas.

  • Tus funciones ahora son servicios que los agentes pueden usar.

  • Por lo tanto, hay dos partes que deberás probar: la forma en que se comporta tu función y la forma en que se comporta el agente.

  • Para tu función, comenzaremos con la prueba de unidades que probablemente ya escribiste, pero hay un par de cosas que debes tener en cuenta cuando el agente es el que realmente realiza la llamada a función.

  • En primer lugar, los valores predeterminados de Kotlin no se aplican aquí.

  • Un parámetro omitido llega como nulo, falso o 0.

  • Por lo tanto, un límite que antes se establecía de forma predeterminada en 10, aquí será 0.

  • La llamada también se inicia en el subproceso principal, y es posible que llamar a tu función desde una prueba no siempre lo haga evidente, por lo que debes ejecutar tu IO estándar en un despachador en segundo plano.

  • Cuando algo falle, arroja un AppFunctionException.

  • Cualquier otro error llegará a tu agente como un error genérico desconocido con el mensaje de excepción sin procesar adjunto.

  • Y tus entradas provendrán de un modelo, por lo que definitivamente deberías probar situaciones de casos extremos, como espacios en blanco, sinónimos de dólares como USD y valores fuera de rango.

  • En el caso del agente, querrás saber si elige tu herramienta en el momento adecuado y si ejecuta la función correctamente.

  • Cada agente se comporta de forma un poco diferente, por lo que es importante que pruebes tus funciones con cualquier agente que esperes que las consuma.

  • Puedes usar nuestro agente de muestra de código abierto para realizar pruebas o puedes usar Gemini para probar esta integración.

Cómo conectarse a Gemini con la Plataforma de apps conectadas

  • Si quieres ver cómo Gemini interactúa con tus AppFunctions o quieres lanzar un MCP de Android en la app de Gemini, puedes hacerlo con la plataforma de apps conectadas, que lanzamos hoy y que actualmente se encuentra en versión preliminar.

  • Para obtener más información, consulta nuestra charla anterior sobre cómo extender tu app a Gemini y la Búsqueda a través de la Connected Apps Platform.

  • Una vez que formes parte del programa de versión preliminar, el proceso para realizar pruebas con Gemini consta de dos pasos.

  • Primero, en Google Connected Console, crearemos una app conectada que Gemini pueda consumir.

  • En segundo lugar, en Play Console, deberás subir tu paquete de aplicación para que podamos acceder a las AppFunctions y puedas comenzar con un segmento de pruebas internas, de modo que los verificadores puedan validar las funciones.

  • Comencemos en Google Connected Console.

  • Aquí, crearás una app conectada con el botón "Crear una app" que se encuentra en la parte superior derecha.

  • Cuando crees la app conectada, elige Android MCP en esta pantalla.

  • Si estuvieras creando una implementación de MCP del servidor, aquí es donde subirías el código del servidor.

  • Cuando hagas clic en "Configurar", todas las apps de Google Play asociadas con tu cuenta de desarrollador aparecerán aquí listas para que las vincules a tu app conectada.

  • Si aún no tienes una app de Google Play, primero deberás crear una en Play Console.

  • Aquí, ya tenemos Jetpacker, así que lo vincularemos.

  • A continuación, agrega los detalles que los usuarios verán en la interfaz de Gemini cuando intenten interactuar con tu app.

  • Esto incluye elementos como el ícono de la app, el nombre visible, las capacidades que tiene tu app conectada y ejemplos de instrucciones que los usuarios pueden usar para interactuar con ella.

  • Deben ser descriptivos para ayudar a guiar mejor a los usuarios.

  • Con esto, concluye la configuración de la app conectada, pero es posible que te preguntes cómo accede la app conectada a las funciones.

  • Como las funciones están contenidas en el código de tu app, ahora tendremos que ir a Play Console, subir el paquete de aplicación y crear un segmento de pruebas para poder probarlo con usuarios reales.

  • En esta pantalla, subiremos el paquete de aplicación y, luego, veremos que Play puede detectar las AppFunctions y que puedes vincularlas directamente a tu app conectada.

  • Ahora, podemos ver la app conectada de Jetpack completa con tus funciones integradas.

  • A continuación, deberás agregar verificadores para que haya alguien listo para validar tus funciones y asegurarse de que funcionen correctamente.

  • Haz clic en las versiones de tu app conectada aquí.

  • En esta pantalla, puedes seleccionar "Administrar verificadores" y configurar una lista limitada de verificadores que podrán acceder a tu app conectada en Gemini una vez que esté disponible.

  • Puedes agregar verificadores a este segmento de pruebas de la misma manera que lo harías para cualquier otra versión.

  • Asegúrate de tener la misma lista de verificadores para la app conectada que para la app de Google Play.

Demostración de la integración de la app de Gemini

  • Ahora, probemos la integración.

  • Aquí está la app de Jetpacker en el teléfono de un usuario, que ya tiene un viaje registrado para esta cumbre.

  • Abriré Gemini para conectar la app.

  • Debo asegurarme de tener instalada en mi dispositivo la misma versión de la app que subimos a Play anteriormente, y debemos usar una cuenta para Gemini que se haya agregado a la lista de verificadores.

  • Aquí, mencionaré a Jetpacker y pediré el número de confirmación de mi habitación de hotel en Mountain View.

  • Gemini llama a las AppFunctions de Jetpacker en el dispositivo y responde: "RED11223 en el Redwood Grove Hotel".

  • A continuación, preguntaré: "¿Cuánto presupuesto me queda para ADS?".

  • Y la llamada a función devolverá $1,850.

  • Ahora, quiero hacer un cambio, así que le pediré a Gemini que agregue USD 45 por el Uber al aeropuerto a mi viaje a ADS.

  • Gemini llama a nuestra función addExpense que vimos antes para agregar esto directamente a mi itinerario.

  • Y cuando abro Jetpacker, el gasto ya está allí y el presupuesto se reduce según corresponda.

  • Si quieres comenzar a crear apps conectadas para Gemini hoy mismo, solicita acceso a la Connected Apps Platform, que actualmente se encuentra en versión preliminar.

Conclusión y futuro de los agentes móviles

  • Bien.

  • En resumen,

  • Por lo tanto, estamos al comienzo de un cambio masivo en la computación móvil, y es el momento de asegurarte de que tu app esté lista.

  • Por lo tanto, comienza por identificar los recorridos del usuario de alto valor principales de tu app.

  • Expón el backend de tu nube con MCP del servidor y usa MCP de Android para trabajar con datos encriptados de extremo a extremo, operaciones locales, sensores de dispositivos y otros elementos integrados en el dispositivo.

  • Para crear prototipos ahora, puedes descargar Android Studio y la CLI de Android.

  • Podrías agregar la dependencia de la versión beta de AppFunctions de AndroidX a tus archivos de compilación y probar tu primera función integrada en el dispositivo de forma local.

  • Para registrarte y comenzar a usar Gemini en producción, puedes registrarte con este código QR.

  • Y, por supuesto, asegúrate de consultar estas charlas relacionadas sobre cómo trabajar con Connected Apps Platform y cómo puedes usar las AppFunctions internas con ML Kit o con tu propio agente para impulsar tu propio agente integrado en la app.

  • Por último, muchas gracias por tu tiempo.

  • Ya queremos ver todas las experiencias increíbles que estás creando para Android.

Código

Fragmentos de código

Fragmento de secuencia de comandos de Kotlin en el minuto 11:56 de "Technical deep dive into Android MCP"
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)
Fragmento de Kotlin en el minuto 12:14 de "Análisis técnico detallado de MCP de Android"
@RequiresApi(36)
@AndroidEntryPoint
@AppFunctionServiceEntryPoint(
    serviceName = "JetPackerAppFunctionService",
    appFunctionXmlFileName = "jetpacker_app_function_service"
)
abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
    @Inject internal lateinit var tripDao: TripDao
    @Inject internal lateinit var expenseDao: ExpenseDao
    ...
Fragmento de Kotlin en el minuto 12:29 de "Análisis técnico detallado de MCP de Android"
/** Represents an event in a trip itinerary. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class TimelineEventSerializable(
  /** The event's unique identifier. */
  val id: String,
  /** The trip ID this event belongs to. */
  val tripId: String,
  /** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
  val type: String,
  /** The timestamp of the event. */
  val timestamp: Long,
  /** The title of the event. */
  val title: String,
  /** The location of the event. */
  val location: String,
  /** A description of the event. */
  val description: String?,
  /** The title of the trip this event belongs to. */
  val tripTitle: String? = null,
  /** Optional title of a matched itinerary event when added as a reminder. */
  val matchedEventTitle: String? = null,
  /** Execution status of the tool call (e.g., SUCCESS). */
  val status: String? = "SUCCESS",
  /** Human-readable completion message instructing the agent not to repeat the call. */
  val message: String? = null,
)
Fragmento de Kotlin en el minuto 12:38 de "Análisis técnico detallado de MCP de Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    val events =
      if (dbEvents.isNotEmpty()) { dbEvents } else {
        val fallback = DummyData.events.filter { it.tripId == targetTripId }
        if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
        fallback
      }
    events.map { it.toSerializable(tripTitle = trip?.title) }
  }
}
Fragmento de Kotlin en el minuto 12:44 de "Análisis técnico detallado de MCP de Android"
/**
 * Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
 * trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
 * specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
 * visitor tips.
 *
 * @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
 * @return A list of events in the itinerary.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
...
Fragmento de Kotlin en el minuto 12:52 de "Análisis técnico detallado de MCP de Android"
/**
 * @param tripId Optional ID of the trip (e.g., '2026-1').
 *   Defaults to the active trip if omitted.
 * @return A list of events in the itinerary.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    // resolveTrip defaults to the active trip if tripId == null
    val trip = resolveTrip(tripId = tripId)
Fragmento de Kotlin en el minuto 13:30 de "Análisis técnico detallado de MCP de Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
    val trip = resolveTrip(tripId = tripId)
    val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?:
    tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
    val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    val events =
      if (dbEvents.isNotEmpty()) { dbEvents } else {
        val fallback = DummyData.events.filter { it.tripId == targetTripId }
        ...
Fragmento de Kotlin en el minuto 13:40 de "Análisis técnico detallado de MCP de Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
    message: Message
): Result {
  ...
  throw AppFunctionException(
    errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
    errorMessage = "To address has invalid syntax!"
  )
}
Fragmento XML en el minuto 14:15 de "Análisis técnico detallado de MCP de Android"
<AppFunctionAppMetadata
xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"

  appfn:description="This app manages trips and travel expenses.

  Operational Patterns:
    - Use 'getAllTrips' to find the correct 'tripId' before
      adding expenses or managing trip details.

  Constraints:
    - Expenses must be associated with a valid trip ID."

  appfn:displayDescription="@string/user_visible_description" />
Fragmento de Kotlin en el minuto 15:00 de "Technical deep dive into Android MCP"
viewModelScope.launch {
    val newState =
        if (enabled) {
            AppFunctionManager.APP_FUNCTION_STATE_ENABLED
        } else {
            AppFunctionManager.APP_FUNCTION_STATE_DISABLED
        }
    appFunctionManager?.setAppFunctionEnabled(id, newState)
}
Fragmento de Kotlin en el minuto 15:12 de "Análisis técnico detallado de MCP de Android"
@AppFunctionSignature(
    scope = AppFunctionMetadata.SCOPE_ACTIVITY,
    appFunctionXmlFileName = "cart_functions"
)
fun interface AddCurrentItemToCart {
    /** Adds the item currently shown on screen to the cart. Returns the new cart size. */
    suspend fun addToCart(quantity: Int): Int
}
Fragmento de Kotlin en el minuto 15:22 de "Análisis técnico detallado de MCP de Android"
val addToCartAppFunctionAdapter =
    appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()

coroutineScope.launch {
    appFunctionManager.handleAppFunction(
        addToCartAppFunctionAdapter.adapt { quantity ->
            val newCartSize = cart.addItem(quantity)
            newCartSize
        }
    )
}
Fragmento de Kotlin en el minuto 15:50 de "Technical deep dive into Android MCP"
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
    level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
    isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
  ...
Fragmento de shell en el minuto 17:05 de "Análisis técnico detallado de MCP de Android"
android skills add appfunctions
Fragmento de Kotlin en el minuto 17:54 de "Análisis técnico detallado de MCP de Android"
/**
 * Adds an expense to a trip.
 *
 * @param tripId ID of the trip. If null, the active trip is used.
 * @param category Expense category. If null, it's inferred from the title.
 */
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun addExpense(
    title: String,
    amount: Double,
    currency: String,
    tripId: String? = null,
    category: String? = null,
): ExpenseSerializable = withContext(Dispatchers.IO) {
    if (amount <= 0.0) {
        throw AppFunctionInvalidArgumentException("amount must be greater than zero")
    }
    val trip = resolveTrip(tripId)
        ?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
    val expense = Expense(trip.id, title, amount, currency,
        category = category ?: inferExpenseCategory(title))
    expenseDao.insertExpense(expense)
    expense.toSerializable(remainingBudgetUsd(trip.id))
}