Applications intelligentes

Intégrer votre application au système d'intelligence Android

Haut-parleurs

À propos

À propos de l'intervention

Android évolue vers un système d'intelligence où des agents comme Gemini orchestrent les tâches entre les applications. Pour débloquer de nouvelles expériences utilisateur et participer à ces workflows agentiques, votre application doit exposer ses fonctionnalités de base.

Cette session est un guide pratique pour intégrer votre application à la couche d'intelligence d'Android. Nous examinerons les principaux chemins d'intégration, y compris le MCP Android sur l'appareil, le MCP côté serveur et l'automatisation des tâches, et nous fournirons un framework de décision pour choisir le chemin adapté à votre cas d'utilisation. Nous explorerons également les expériences proactives avec le Contexte personnel.

Vous allez apprendre à effectuer les tâches suivantes :

  • Sélectionnez le parcours d'intégration optimal pour les besoins de votre application.
  • Implémentez des fonctionnalités d'intelligence réactives et proactives.
  • Équilibrer les performances et la sécurité dans les intégrations agentiques

Résumé

  • Android MCP est un framework d'appel d'outils unifié sur l'appareil, optimisé par la bibliothèque Jetpack AppFunctions. Il mappe les primitives MCP aux constructions Kotlin et s'exécute sur l'IPC Android Binder.
  • Les outils sur l'appareil sont particulièrement efficaces pour accéder aux données chiffrées ou aux capteurs de l'appareil. L'automatisation des tâches basée sur la plate-forme permet également aux agents préchargés de simuler des événements tactiles sans modifier le code.
  • Votre application agit en tant que serveur MCP Android, tandis que les agents découvrent et appellent des outils. Déclarez les outils avec AppFunctionDeclaration, fournissez des descriptions KDoc et évitez de vous fier aux valeurs par défaut de Kotlin.
  • La Connected Apps Platform, actuellement en version preview, vous permet de tester vos AppFunctions avec Gemini. Configurez votre application connectée dans la Google Connected Console, puis importez votre bundle dans la Play Console.

Ressources

Gemini Notebook

Thèmes

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

Transcription

Présentation de l'ère de l'intelligence

  • Très bien.

  • Bonjour !

  • Je m'appelle Dan Galpin et je travaille dans l'équipe Developer Relations.

  • Je m'appelle Sarah Shamsie et je suis responsable produit pour les frameworks d'intelligence des développeurs Android.

  • Nous sommes ravis de vous accueillir à la session d'aujourd'hui.

  • Nous assistons à un changement de paradigme dans la façon dont les utilisateurs interagissent avec leurs appareils.

  • À l'ère du Web, les utilisateurs commençaient généralement leur parcours en ouvrant le navigateur Web, puis en utilisant la fonction de recherche.

  • Puis, à l'ère du mobile, le comportement des utilisateurs a évolué vers l'utilisation d'applications natives plus spécialisées pour effectuer des actions ponctuelles.

  • Voici un exemple.

  • Je reçois un message d'un ami qui m'invite à dîner.

  • Pour répondre, je dois vérifier mon agenda pour voir si je suis libre, ajouter l'événement à mon agenda, réserver un trajet si j'ai besoin d'un moyen de transport, puis envoyer un message à mon ami pour lui dire que je peux venir.

  • Auparavant, cela nécessitait de passer d'une application à l'autre sur mon appareil pour effectuer les actions et finaliser mon parcours.

  • Mais nous sommes désormais dans l'ère de l'intelligence.

  • À l'ère de l'intelligence, les utilisateurs commencent leur parcours en exprimant des intentions et des objectifs généraux plutôt que des actions spécifiques à effectuer.

  • On assiste à un changement fondamental : les utilisateurs considèrent désormais leur appareil comme un outil qui anticipe de manière proactive leurs besoins et les aide à atteindre leurs objectifs, plutôt que comme un simple moyen d'effectuer des actions.

  • Nous avons remarqué qu'ils s'attendent à ce que ce framework d'intelligence fonctionne non seulement sur leurs appareils mobiles, mais aussi sur un riche écosystème de facteurs de forme, y compris les montres, les voitures, les téléviseurs, les ordinateurs portables, la XR et les lunettes.

  • À l'ère de l'intelligence, une seule requête multi-application peut en fait faciliter l'exemple d'invitation à dîner mentionné précédemment.

  • Avec une seule requête, un agent sur mon appareil peut m'aider à effectuer toutes les actions dont j'ai besoin : consulter mon agenda, ajouter l'événement, réserver le trajet et envoyer un message, le tout en une seule fois.

  • Aujourd'hui, nous allons vous en dire plus sur les projets d'Android dans ce domaine et sur la façon dont vous, en tant que développeurs, pouvez créer des expériences de premier ordre.

  • Dan.

  • Merci, Sarah.

  • Android continue donc d'ajouter de l'intelligence à son cœur, ce qui offre de nouvelles façons d'équiper vos applications pour cette ère de l'intelligence.

  • Le premier de ces deux piliers de l'intelligence pour les développeurs est l'intelligence intégrée aux applications. Il s'agit évidemment de rendre vos applications plus intelligentes, d'y intégrer de l'intelligence. Vous pouvez utiliser des modèles sur l'appareil comme Gemini Nano 4 via ML Kit pour résumer des fils de discussion, réécrire des réponses et comprendre des images, ou utiliser le langage naturel pour appeler les fonctions de votre application.

  • Vous pouvez même utiliser les modèles Gemini dans le cloud directement dans votre application grâce à Firebase AI Logic.

  • Mais aujourd'hui, nous allons nous concentrer sur le deuxième pilier : s'assurer que les fonctionnalités de votre application sont disponibles dans le système d'intelligence.

  • Alors, qu'est-ce qu'un système d'intelligence ?

  • En plus de proposer les expériences suggérées dont Sarah a parlé plus tôt, il s'agit d'un système qui permet des intégrations profondes pour améliorer les expériences dans l'écosystème d'applications.

  • Ainsi, les appareils conçus pour l'intelligence Gemini, par exemple, exploitent ces points d'intégration pour fournir des suggestions, des résumés de notifications, une saisie vocale améliorée et le remplissage automatique des formulaires.

  • Les suggestions font désormais partie d'une fonctionnalité appelée "assistance proactive".

  • Jusqu'à présent, nous avons parlé de l'assistance sollicitée.

  • Nous sommes tous habitués à cela : un utilisateur demande à un agent d'accomplir une ou plusieurs tâches en son nom.

  • Mais l'assistance proactive consiste en fait à anticiper la ou les prochaines actions qu'un utilisateur pourrait effectuer.

  • Pour reprendre l'exemple du dîner, l'agent peut proposer de manière proactive des suggestions à l'utilisateur en réponse au message, par exemple pour ajouter l'invitation à son agenda ou réserver un covoiturage.

  • Mais comment invite-t-il l'utilisateur à agir ?

  • L'assistance proactive peut afficher des actions à différents endroits, comme l'écran de verrouillage, les notifications, les claviers virtuels et même certaines applications.

  • Alors, comment faire pour que votre application apparaisse dans ces chips proactifs afin de générer du trafic ?

  • Vous utilisez déjà la dernière version du SDK et nos outils les plus récents, mais la meilleure chose à faire maintenant est de vous assurer que votre application est également prête pour l'assistance par requête, dont Sarah va parler ensuite.

Assistance par prompt et MCP Android

  • Merci, Dan.

  • D'accord. L'assistance sur demande implique donc que les utilisateurs s'adressent directement à un agent et lui demandent de lancer et d'orchestrer des tâches, même entre différentes applications.

  • Cela peut automatiser des workflows auparavant manuels et chronophages.

  • Pour notre système d'intelligence sur Android, nous nous concentrons principalement sur les agents système comme Gemini, qui peuvent exploiter les applications sur l'appareil de l'utilisateur.

  • Votre application peut s'afficher de différentes manières dans cette expérience et interagir avec l'agent système.

  • Il existe notre solution d'automatisation des tâches basée sur la plate-forme, ainsi que des intégrations d'API directes dans l'agent système.

  • Nous vous recommandons de toujours créer des intégrations d'API directes dans la mesure du possible.

  • Ces éléments sont contrôlés par le développeur. Vous pouvez donc décider et contrôler la façon dont votre application et votre marque s'affichent.

  • Elles sont également plus rapides et plus fiables, et offrent une expérience plus haut de gamme à vos utilisateurs.

  • Vous pouvez utiliser deux chemins d'API pour l'intégration à l'agent.

  • Le protocole MCP (Model Context Protocol) est la bonne pratique du secteur pour l'appel de fonction côté serveur.

  • Pour l'appel d'outils sur l'appareil, nous annonçons officiellement Android MCP, le framework d'appel de fonction unifié sur Android.

  • Android MCP est optimisé par notre bibliothèque Jetpack AppFunctions, qui s'aligne étroitement, tant sur le plan technique que conceptuel, sur MCP, tout en prenant en charge une implémentation Kotlin familière et idiomatique que de nombreux développeurs Android connaissent.

  • L'implémentation de l'API est donc relativement intuitive et facile à utiliser, et vous pouvez exploiter les connaissances que vous avez acquises lors d'une intégration MCP côté serveur déjà en place.

  • De nombreuses primitives MCP correspondent à une construction Jetpack AppFunctions.

  • Prenons quelques exemples pour illustrer ce point.

  • Un point de terminaison d'outil MCP correspond parfaitement à une fonction Kotlin annotée avec AppFunctionDeclaration.

  • Vos modèles pydantic sont mappés à des classes de données Kotlin annotées avec AppFunctionSerializable.

  • Mais au lieu d'utiliser le transport HTTP en flux et/ou les entrées/sorties standards, Android MCP s'exécute sur l'IPC Android Binder optimisé.

  • Aujourd'hui, nous sommes ravis de vous annoncer que la bibliothèque Jetpack AppFunctions est désormais disponible en version bêta.

Principaux cas d'utilisation de MCP Android

  • Vous vous demandez probablement quand il est judicieux d'utiliser un chemin d'API et quand il est préférable d'utiliser Android MCP.

  • En règle générale, Android MCP est particulièrement utile lorsque vous souhaitez exploiter le contexte local ou sur l'appareil pour répondre aux besoins d'un parcours utilisateur.

  • Examinons quelques exemples où le MCP Android se distingue particulièrement.

  • Plus précisément, vous devriez envisager d'utiliser Android MCP lorsque vous devez accéder à des données locales chiffrées.

  • C'est particulièrement important pour les applications de messagerie et de communication, où l'historique des messages doit rester sur l'appareil en raison du chiffrement de bout en bout.

  • Android MCP est également utile pour les actions purement sur l'appareil et hors connexion, lorsque les agents les prennent en charge.

  • Notre partenaire Wear OS, Samsung Health, a lancé une fonction permettant de démarrer une course et des exercices, entièrement orchestrés sur l'appareil.

  • Les utilisateurs veulent souvent que ces types d'actions soient effectués très rapidement, avec une fiabilité élevée, une faible latence et parfois même dans des environnements à faible connectivité. Le fait de s'assurer que l'ensemble de l'appel d'outil, du routage et du routage des requêtes peut se faire sur l'appareil permet de garantir cela.

  • Android MCP est également le chemin d'API privilégié si vous souhaitez accéder aux capteurs de l'appareil ou interagir directement avec son matériel.

  • Par exemple, de nombreuses applications automobiles souhaitent permettre aux utilisateurs de déverrouiller instantanément les portières de leur voiture grâce à la connectivité UWB, et souvent en mode mains libres.

  • C'est là qu'intervient Android MCP.

  • Enfin, c'est la solution qu'il vous faut si aucun point de terminaison cloud n'est disponible pour vos applications ou votre API.

  • De nombreuses applications first party de nos fabricants d'appareils sont uniquement côté client. C'est pourquoi ils utilisent Android MCP.

  • Par exemple, Gemini peut être utilisé pour rechercher et récupérer des photos dans des applications de galerie côté client uniquement, comme Xiaomi Gallery.

  • Ici, je peux ouvrir Gemini et lui demander : "Trouve les photos du robot Android que j'ai prises la semaine dernière dans Xiaomi Gallery."

  • Xiaomi a exposé une fonction de l'application pour rechercher des photos, que Gemini appelle ensuite. Les photos pertinentes sont renvoyées et affichées directement dans l'interface utilisateur de l'application Gemini, de sorte que l'utilisateur n'a jamais à la quitter.

  • Nous nous appuyons sur la même architecture pour les actions sur l'appareil dans l'OS Android.

  • Le protocole MCP Android alimente la fonctionnalité d'aide sur l'appareil récemment lancée, qui est une intégration directe de l'OS Android dans l'application Gemini.

  • Cette fonctionnalité a déjà été lancée sur les appareils Pixel et Lenovo, et elle est actuellement déployée sur l'ensemble de l'écosystème des OEM Android.

  • Les utilisateurs peuvent utiliser l'Aide sur l'appareil en accédant à Gemini et en demandant comment optimiser leurs paramètres, les paramètres de leur appareil et les paramètres système pour améliorer leur expérience sur l'appareil.

  • Par exemple, au lieu de parcourir différents menus, un utilisateur daltonien peut demander à Gemini : "Je suis daltonien.

  • Comment puis-je améliorer mon expérience sur ce téléphone ?"

  • Gemini appellera et invoquera les fonctions de l'application locale, analysera intelligemment la requête, et suggérera des moyens concrets de modifier les paramètres système et les paramètres de l'appareil pour améliorer l'expérience.

  • Ensuite, les commandes intégrées à l'application s'affichent directement dans l'UI de l'application Gemini, ce qui permet aux utilisateurs de prendre immédiatement le contrôle.

  • Par exemple, pour répondre à cette demande d'optimisation pour les daltoniens, Gemini a affiché un bouton d'activation de la correction des couleurs, afin que les utilisateurs puissent effectuer la modification à la volée.

  • Les expériences MCP Android sont déjà disponibles sur certains appareils, et nous collaborons avec plusieurs partenaires pour proposer encore plus de cas d'utilisation à l'écosystème Android.

  • Bien que les déclarations explicites d'outils MCP Android soient notre référence, nous savons qu'il y aura des cas où les utilisateurs demanderont aux agents d'effectuer des cas d'utilisation complexes ou moins courants. C'est là qu'intervient notre fonctionnalité d'automatisation des tâches basée sur la plate-forme.

  • Dans ce modèle, l'agent préchargé par l'OEM peut utiliser les API de contrôle de l'ordinateur d'Android pour simuler des événements tactiles sur une surface d'application.

  • Cela ne nécessite aucune modification de code de la part des développeurs. L'agent doit appuyer sur l'UI comme le ferait un utilisateur, en analysant l'écran de l'application.

  • Pour garantir le contrôle et la sécurité des utilisateurs, il est essentiel que les agents s'interrompent lors d'actions potentiellement sensibles, comme les flux de paiement, et demandent la confirmation de l'utilisateur pour continuer.

  • Voyons un exemple en action.

  • Ici, un utilisateur a pris une photo d'une visite sur le thème du café et du chocolat, et il souhaite demander à Expedia, ou demander à Gemini de rechercher sur Expedia, s'il peut trouver une visite similaire, mais en ajustant le nombre de personnes à six participants.

  • Sans intégration d'API directe, Gemini parcourt l'interface utilisateur d'Expedia en arrière-plan, de la même manière qu'un utilisateur.

  • Il affiche ensuite les options pertinentes qu'il a trouvées et passe la main à l'utilisateur pour la confirmation finale et la réservation.

  • Il est important de comprendre que ces trois approches ne s'excluent pas mutuellement et qu'elles sont en fait plus efficaces lorsqu'elles sont combinées.

  • Les utilisateurs et les développeurs doivent s'attendre à ce que les agents puissent coordonner les outils entre les implémentations de serveur et l'application Android locale pour offrir l'expérience la plus complète et la plus globale possible aux utilisateurs.

Présentation technique détaillée de MCP Android

  • Très bien.

  • Merci, Sarah.

  • Nous allons nous pencher plus en détail sur Android MCP.

  • Et quand c'est possible, je veux que vous sachiez que je récupère les extraits de code MCP de Jetpacker, notre application exemple de voyage, qui est en fait conçu avec l'aide d'Antigravity.

  • Vous l'avez vu dans la keynote.

  • Il est doté de toutes sortes de fonctionnalités intelligentes, y compris des intégrations sur l'appareil avec les API ML Kit IA générative et ML Kit de reconnaissance vocale.

  • Désormais, le MCP Android et le MCP dans le cloud ont une structure similaire.

  • Les deux ont des clients et des serveurs.

  • Dans le monde de MCP, les serveurs fournissent des outils, et les clients peuvent les découvrir et les appeler.

  • Désormais, avec Android MCP, votre application est le serveur et l'agent est le client.

  • La plate-forme Android joue le rôle de registre d'outils.

  • Les applications commencent par enregistrer un service pour fournir le serveur. Nous allons commencer par ajouter les dépendances de la bibliothèque Jetpack AppFunctions dans notre exemple ici.

    implementation(libs.androidx.appfunctions)
    ksp(libs.androidx.appfunctions.compiler)
  • Nous utiliserons ensuite des annotations KSP pour créer un AppFunctionService.

  • Ce service permet à vos fonctions d'application de respecter correctement les cycles de vie Android.

  • Nous pouvons utiliser l'injection de dépendances avec Hilt pour injecter des dépôts de données que nos AppFunctions peuvent utiliser.

    @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
        ...
  • Désolé.

  • Revenons à notre diagramme.

  • Une fois le service configuré pour notre serveur MCP Android, l'application enregistre les outils sous forme d'AppFunctions, ainsi que tous les modèles de données associés qu'ils utilisent.

  • Vous pouvez maintenant enregistrer un modèle de données à l'aide de l'annotation AppFunctionSerializable, et ce KDoc est plutôt sympa.

    /** 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,
    )
  • Il s'agit simplement de métadonnées utilisées par l'agent pour comprendre comment utiliser le modèle.

  • Vous pouvez utiliser l'annotation AppFunctionDeclaration pour enregistrer votre fonction en tant qu'outil.

    @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) }
      }
    }
  • Et avec isDescribedByKDoc défini sur "true", KDoc fournit à l'agent une description pour comprendre ce que fait votre fonction, et aide également les humains à la comprendre.

    /**
     * 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()
    ...
  • Voici quelques bonnes pratiques pour la conception des paramètres.

    /**
     * @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)
  • Tout d'abord, vous devez vous concentrer sur le sous-ensemble minimal de paramètres avec des noms clairs et distincts.

  • Rendez les paramètres non essentiels nullables et résolvez leurs valeurs par défaut dans le corps de la fonction.

  • Il ne s'agit pas seulement d'une bonne pratique.

  • Cela ne fonctionnera pas, car les valeurs par défaut de Kotlin seront ignorées, car la bibliothèque AppFunctions transmet une valeur nulle pour les paramètres nullable omis.

  • Je ne vais même pas vous montrer d'exemple pour le premier, mais n'utilisez pas de paramètres génériques.

  • Des éléments comme le contexte complet ou l'historique des conversations sont tout simplement mauvais.

  • Ils peuvent amener les agents à transmettre de grandes quantités de données et entraîner une exfiltration, ce qui est une mauvaise pratique.

  • Étant donné que l'AppFunction s'exécute par défaut sur le thread UI, vous devez vous assurer que toute opération de longue durée s'exécute en arrière-plan en choisissant le coordinateur de coroutine approprié.

    @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 }
            ...
  • Vous souhaitez générer des exceptions AppFunction si nécessaire, avec des informations détaillées pour que l'agent puisse comprendre comment résoudre une erreur.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun send(
        message: Message
    ): Result {
      ...
      throw AppFunctionException(
        errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
        errorMessage = "To address has invalid syntax!"
      )
    }
  • Maintenant que nous avons une AppFunction, revenons à notre diagramme de l'architecture.

  • L'application cliente de l'agent s'enregistre auprès de la plate-forme pour appeler des AppFunctions.

  • C'est le cas de Gemini, par exemple.

  • Pour ce faire, ils doivent disposer d'une autorisation privilégiée. Les agents enregistrés peuvent ensuite rechercher les métadonnées AppFunction sur la plate-forme Android et décider s'ils doivent exécuter une ou plusieurs AppFunctions enregistrées pour répondre à la demande d'un utilisateur.

  • En parlant d'une ou de plusieurs AppFunctions, comment l'agent prend-il sa décision ?

  • Il vous reste une dernière chose à faire : décrire les fonctionnalités de votre application dans un fichier de métadonnées.

    <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" />
  • Les agents sélectionnent les outils en lisant d'abord cette description dans votre AppFunctionAppMetadata, puis en lisant les descriptions des différentes AppFunctions.

  • Vous devez donc commencer votre description par une brève explication du domaine principal de votre application ou de vos outils.

  • Vous pouvez ensuite ajouter des modèles opérationnels pour aider l'agent à comprendre le flux logique des opérations. Cela permet de définir les conditions préalables ou d'orienter l'agent vers les chemins optimaux pour accomplir les tâches.

  • Vous pouvez également ajouter une section sur les contraintes qui définit les limites logiques des types de données d'entrée ou renforce les modèles opérationnels.

  • Voilà pour les principes de base de la plate-forme MCP Android, mais vous pouvez faire d'autres choses.

  • Vous pouvez activer et désactiver les AppFunctions de manière dynamique, ce qui est plutôt pratique.

    viewModelScope.launch {
        val newState =
            if (enabled) {
                AppFunctionManager.APP_FUNCTION_STATE_ENABLED
            } else {
                AppFunctionManager.APP_FUNCTION_STATE_DISABLED
            }
        appFunctionManager?.setAppFunctionEnabled(id, newState)
    }
  • C'est important, par exemple, si vous souhaitez qu'AppFunction ne soit disponible que si l'utilisateur est connecté.

  • Vous pouvez également créer des AppFunctions limitées à un écran ou à une activité spécifiques de votre application.

    @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
    }
  • Ainsi, au lieu de déclarer l'AppFunction, vous déclarez AppFunctionSignature.

  • Vous pouvez ensuite implémenter un gestionnaire pour l'AppFunction dans votre UI.

    val addToCartAppFunctionAdapter =
        appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
    
    coroutineScope.launch {
        appFunctionManager.handleAppFunction(
            addToCartAppFunctionAdapter.adapt { quantity ->
                val newCartSize = cart.addItem(quantity)
                newCartSize
            }
        )
    }
  • L'avantage, c'est que le cycle de vie des gestionnaires est automatiquement lié à celui du champ d'application de coroutine que vous utilisez.

  • Vous allez maintenant mettre en cache cet adaptateur de requête AppFunctions (désolé, l'adaptateur de requête AppFunctions de gestion), car la bibliothèque utilise la réflexion en arrière-plan pour l'instancier.

  • Il existe également une fonctionnalité permettant de créer des AppFunctions limitées dont l'enregistrement n'est pas directement lié au cycle de vie Android.

  • Dernier point : nous avons un élément appelé "niveau d'accès".

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
      ...
  • Vous pouvez utiliser le niveau d'accès "self", "system" ou "trusted". Cela indique quelles fonctions sont réservées à votre propre agent intégré à l'application (par exemple, un agent créé avec ML Kit GenAI), quelles fonctions ne sont disponibles que pour le système et quelles fonctions sont disponibles pour les agents système de confiance.

  • Pour vous aider à créer votre propre agent intégré, nous avons déjà annoncé que ML Kit est compatible de manière native avec AppFunctions dans le cadre de son framework d'appel d'outils unifié.

  • Vous les définissez une seule fois et bénéficiez d'une couverture à la fois pour les agents au niveau du système et les modèles locaux intégrés à l'application, ce qui vous permet d'éviter les déclarations d'outils fragmentées.

  • Pour en savoir plus, vous pouvez regarder cette présentation, "Unlock agentic capabilities in your app with ML Kit GenAI" (Débloquer des capacités agentiques dans votre application avec ML Kit GenAI), ou simplement découvrir les dernières nouveautés de Jetpack.

  • C'est plutôt cool.

  • Revenons à Sarah.

  • Très bien.

  • Dan vient de vous montrer comment fonctionne MCP pour Android en arrière-plan.

  • Passez à la pratique.

  • N'oubliez pas que votre application est un serveur et que les agents sont les clients. L'objectif est donc de vous assurer que votre application est un bon serveur pour eux.

  • Nous allons suivre Jetpacker, notre application exemple de voyage, de la première déclaration d'outil à un appel de fonction réel via Gemini.

  • Vous pouvez écrire vos AppFunctions manuellement ou utiliser un agent de codage pour vous aider à démarrer.

  • Nous avons lancé notre compétence AppFunctions, qui peut vous aider à commencer à écrire vos fonctions. Vous pouvez les associer à Android Studio via la CLI Android ou les utiliser avec l'agent de codage de votre choix.

    android skills add appfunctions
  • La compétence fonctionne en trois étapes.

  • Tout d'abord, il analyse votre codebase pour identifier les parcours utilisateur principaux qui pourraient être utiles, comme la lecture de contenus multimédias, la création d'une note ou l'envoi d'un message.

  • Ensuite, il génère des déclarations AppFunction qui exposent ces parcours, avec l'implémentation et la compatibilité de la plate-forme dont ils ont besoin.

  • Par exemple, Gradle et KSP, le fichier XML de métadonnées de l'application et l'injection de dépendances.

  • Enfin, il affinera vos descriptions KDoc, car c'est exactement ce que l'agent utilise pour décider quand appeler votre fonction et comment.

  • Voici le type de résultat que nous recherchons.

  • Il s'agit d'une fonction addExpense pour 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 description est une phrase d'une ligne qui commence par un verbe et une ressource : "ajoute une dépense à un voyage".

  • Les champs "trip" et "category" peuvent être nuls, car les utilisateurs ne les spécifient pas toujours, mais la fonction les résoudra dans le corps.

  • Lorsqu'une requête échoue, elle génère une AppFunctionException pour que l'agent sache ce qui s'est mal passé.

Tester les fonctions et les agents de l'application

  • Passons maintenant aux tests.

  • Vos fonctions sont désormais des services que les agents peuvent utiliser.

  • Vous devrez donc tester deux éléments : le comportement de votre fonction et celui de l'agent.

  • Pour votre fonction, nous allons commencer par le test unitaire que vous avez probablement déjà écrit, mais il y a quelques points à garder à l'esprit lorsque l'agent effectue l'appel de fonction.

  • Tout d'abord, les valeurs par défaut de Kotlin ne s'appliquent pas ici.

  • Un paramètre omis arrive sous la forme d'une valeur nulle, fausse ou 0.

  • Ainsi, une limite qui était auparavant définie par défaut sur 10 sera en fait de 0 ici.

  • L'appel démarre également sur le thread principal, et l'appel de votre fonction à partir d'un test n'est pas toujours évident. Vous devez donc exécuter vos E/S standards sur un coordinateur en arrière-plan.

  • En cas d'échec, générez une AppFunctionException.

  • Tout autre élément parviendra à votre agent sous la forme d'une erreur inconnue générique, avec votre message d'exception brut en pièce jointe.

  • Vos entrées proviendront d'un modèle. Vous devez donc absolument tester des scénarios de cas particuliers, comme des espaces vides, des synonymes de "dollars" comme "USD" et des valeurs hors plage.

  • Du côté de l'agent, vous devez savoir s'il choisit votre outil au bon moment et s'il exécute correctement la fonction.

  • Chaque agent se comporte un peu différemment. Il est donc important de tester vos fonctions avec tous les agents qui sont censés les utiliser.

  • Vous pouvez utiliser notre exemple d'agent Open Source ou Gemini pour tester cette intégration.

Se connecter à Gemini avec Connected Apps Platform

  • Si vous souhaitez voir comment Gemini interagit avec vos AppFunctions ou si vous souhaitez lancer un Android MCP sur l'application Gemini, vous pouvez le faire avec la Connected Apps Platform, que nous avons lancée plus tôt aujourd'hui et qui est actuellement en preview.

  • Pour en savoir plus, consultez notre précédente présentation sur l'extension de votre application à Gemini et à la Recherche via la Connected Apps Platform.

  • Une fois que vous faites partie du programme Preview, le processus pour tester Gemini ne comporte que deux étapes.

  • Tout d'abord, dans la Google Connected Console, nous allons créer une appli connectée que Gemini pourra utiliser.

  • Ensuite, dans la Play Console, nous devrons importer votre app bundle pour pouvoir accéder aux AppFunctions. Vous pourrez alors lancer un canal de test interne pour que les testeurs puissent valider les fonctions.

  • Commençons par la Google Connected Console.

  • Ici, vous allez créer une application connectée à l'aide du bouton "Créer une application" en haut à droite.

  • Lorsque vous créez l'application connectée, sélectionnez "Android MCP" sur cet écran.

  • Si vous développiez une implémentation MCP côté serveur, c'est ici que vous importeriez votre code de serveur.

  • Lorsque vous cliquez sur "Configurer", toutes les applications Google Play associées à votre compte de développeur s'affichent ici, prêtes à être associées à votre application connectée.

  • Si vous n'avez pas encore d'application Google Play, vous devez d'abord en créer une dans la Play Console.

  • Ici, nous avons déjà Jetpacker. Nous allons donc l'associer.

  • Ensuite, ajoutez les détails que les utilisateurs verront dans l'interface Gemini lorsqu'ils essaieront d'interagir avec votre application.

  • Cela inclut l'icône d'application, un nom à afficher, les capacités de votre application connectée et des exemples de prompts que les utilisateurs peuvent utiliser pour interagir avec elle.

  • Ils doivent être descriptifs pour guider au mieux les utilisateurs.

  • Voilà qui conclut la configuration de l'appli connectée. Vous vous demandez peut-être comment l'appli connectée accède aux fonctions.

  • Comme les fonctions sont contenues dans le code de votre application, nous devons maintenant accéder à la Play Console, importer votre app bundle et créer un canal de test afin de pouvoir le tester avec de vrais utilisateurs.

  • Sur cet écran, nous allons importer l'app bundle. Nous verrons ensuite que Play peut détecter les AppFunctions et que vous pouvez les associer directement à votre appli connectée.

  • L'application connectée Jetpack s'affiche désormais avec vos fonctions intégrées.

  • Ensuite, nous devrons ajouter des testeurs pour que quelqu'un puisse valider vos fonctions et s'assurer qu'elles fonctionnent correctement.

  • Cliquez donc sur les versions de votre application connectée ici.

  • Sur cet écran, vous pouvez sélectionner "Gérer les testeurs" et configurer une liste limitée de testeurs qui pourront accéder à votre appli connectée dans Gemini une fois qu'elle sera disponible.

  • Vous pouvez ajouter des testeurs à ce canal de test de la même manière que pour n'importe quelle autre version.

  • Assurez-vous d'avoir la même liste de testeurs pour l'application connectée que pour votre application Google Play.

Démonstration de l'intégration de l'application Gemini

  • Maintenant, testons l'intégration.

  • Voici l'application Jetpacker sur le téléphone d'un utilisateur, avec un voyage déjà enregistré pour ce sommet.

  • Je vais ouvrir Gemini pour connecter l'appli.

  • Je dois m'assurer que la version de l'application installée sur mon appareil est la même que celle que nous avons importée dans Play précédemment, et que nous utilisons un compte pour Gemini qui a été ajouté à la liste des testeurs.

  • Ici, je vais mentionner Jetpacker et demander le numéro de confirmation de ma chambre d'hôtel à Mountain View.

  • Gemini appelle les AppFunctions de Jetpacker sur l'appareil et répond "RED11223 au Redwood Grove Hotel".

  • Ensuite, je vais demander : "Quel est le budget restant pour ADS ?"

  • L'appel de fonction renverra 1 850 $.

  • Maintenant, je veux apporter une modification. Je vais donc demander à Gemini d'ajouter 45 $ pour le trajet Uber vers l'aéroport à mon voyage à ADS.

  • Gemini appelle notre fonction addExpense précédente pour l'ajouter directement à mon itinéraire.

  • Et quand j'ouvre Jetpacker, la dépense est déjà là, et le budget est réduit en conséquence.

  • Si vous souhaitez commencer à créer des applications connectées pour Gemini dès aujourd'hui, demandez l'accès à la Connected Apps Platform, actuellement en preview.

Conclusion et avenir des agents mobiles

  • Très bien.

  • Pour conclure,

  • Nous sommes donc au début d'une transition majeure dans l'informatique mobile, et il est temps de vous assurer que votre application est prête.

  • Commencez donc par identifier les principaux parcours utilisateur à forte valeur ajoutée de votre application.

  • Exposez le backend de votre cloud à l'aide du MCP côté serveur, et utilisez le MCP Android pour travailler avec des données chiffrées de bout en bout, des opérations locales, des capteurs d'appareil et d'autres éléments sur l'appareil.

  • Pour créer un prototype dès maintenant, vous pouvez télécharger Android Studio et Android CLI.

  • Vous pouvez ajouter la dépendance bêta AndroidX AppFunctions à vos fichiers de compilation et tester votre première fonction sur l'appareil localement.

  • Pour vous inscrire et commencer à utiliser Gemini en production, vous pouvez scanner ce QR code.

  • Et bien sûr, ne manquez pas ces présentations connexes sur l'utilisation de Connected Apps Platform, et sur la façon dont vous pouvez utiliser les AppFunctions internes avec ML Kit ou avec votre propre agent pour piloter votre propre agent intégré à l'application.

  • Enfin, merci beaucoup pour le temps que vous nous avez consacré.

  • Nous avons hâte de découvrir toutes les expériences exceptionnelles que vous allez créer pour Android.

Code

Extraits de code

Extrait de script Kotlin à 11:56 dans "Présentation détaillée de Android MCP"
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)
Extrait Kotlin à 12:14 dans "Technical deep dive into Android MCP"
@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
    ...
Extrait de code Kotlin à 12:29 dans "Technical deep dive into Android MCP"
/** 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,
)
Extrait de code Kotlin à 12:38 dans "Présentation détaillée technique d'Android MCP"
@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) }
  }
}
Extrait de code Kotlin à 12:44 dans "Technical deep dive into Android MCP"
/**
 * 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()
...
Extrait de code Kotlin à 12:52 dans "Technical deep dive into Android MCP"
/**
 * @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)
Extrait de code Kotlin à 13:30 dans "Présentation détaillée technique d'Android MCP"
@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 }
        ...
Extrait de code Kotlin à 13:40 dans "Technical deep dive into Android MCP"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
    message: Message
): Result {
  ...
  throw AppFunctionException(
    errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
    errorMessage = "To address has invalid syntax!"
  )
}
Extrait XML à 14:15 dans "Présentation détaillée technique d'Android MCP"
<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" />
Extrait Kotlin à 15:00 dans "Présentation détaillée 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)
}
Extrait de code Kotlin à 15:12 dans "Technical deep dive into Android MCP"
@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
}
Extrait Kotlin à 15:22 dans "Technical deep dive into Android MCP"
val addToCartAppFunctionAdapter =
    appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()

coroutineScope.launch {
    appFunctionManager.handleAppFunction(
        addToCartAppFunctionAdapter.adapt { quantity ->
            val newCartSize = cart.addItem(quantity)
            newCartSize
        }
    )
}
Extrait Kotlin à 15:50 dans "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) {
  ...
Extrait de shell à 17:05 dans "Technical deep dive into Android MCP"
android skills add appfunctions
Extrait de code Kotlin à 17:54 dans "Technical deep dive into Android MCP"
/**
 * 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))
}