Product Manager di Google
Android si sta evolvendo in un sistema intelligente in cui agenti come Gemini coordinano le attività tra le app. Per sbloccare nuove esperienze utente e partecipare a questi agentic workflow, la tua app deve esporre le sue funzionalità principali.
Questa sessione è una guida pratica all'integrazione della tua app con il livello di intelligence di Android. Esamineremo i principali percorsi di integrazione, tra cui MCP Android on-device, MCP lato server e automazione delle attività, e forniremo un framework decisionale per scegliere il percorso giusto per il tuo caso d'uso. Esploreremo anche le esperienze proattive con Contesto personale.
Imparerai come:
AppFunctionDeclaration, fornisci descrizioni KDoc ed evita di fare affidamento sui valori predefiniti di Kotlin.Trascrizione
Okay.
Ciao,
Sono Dan Galpin di Developer Relations.
E io sono Sarah Shamsie, product manager che lavora sui framework di intelligence per gli sviluppatori di Android.
Siamo felici di darti il benvenuto alla sessione di oggi.
Stiamo assistendo a un cambiamento di paradigma nel modo in cui gli utenti interagiscono con i loro dispositivi.
Nell'era del web, gli utenti in genere iniziavano i loro percorsi accedendo al browser web e poi utilizzando la funzione di ricerca.
Poi, nell'era dei dispositivi mobili, abbiamo assistito a un cambiamento nel comportamento degli utenti, che hanno iniziato a utilizzare app native più specializzate per completare azioni una tantum.
Ecco un esempio.
Arriva un messaggio da un amico che mi invita a cena.
Per rispondere, devo controllare il mio calendario per vedere se sono libera, aggiungere l'evento al calendario, prenotare una corsa se ho bisogno di un mezzo per arrivarci e poi rispondere alla mia amica che posso partecipare.
In precedenza, questo richiedeva il cosiddetto app-hopping, ovvero dovevo passare da un'app all'altra sul mio dispositivo per completare effettivamente le azioni e portare a termine il mio percorso.
Ma ora siamo nell'era dell'intelligence.
Nell'era dell'AI, gli utenti iniziano i loro percorsi esprimendo intenti e obiettivi di alto livello, anziché azioni specifiche da compiere.
C'è un cambiamento fondamentale nel modo in cui vedono il loro dispositivo: non più come qualcosa che consente semplicemente di compiere azioni, ma come qualcosa che anticipa attivamente le loro esigenze e li aiuta a raggiungere i loro obiettivi.
Abbiamo notato che si aspettano che questo framework di AI funzioni non solo sui loro dispositivi mobili, ma anche su un ricco ecosistema di fattori di forma, tra cui smartwatch, auto, TV, laptop, XR e occhiali.
Nell'era dell'AI, un singolo prompt cross-app può effettivamente facilitare l'esempio dell'invito a cena di prima.
Con un solo prompt, un agente sul mio dispositivo può aiutarmi ad attivare tutte le azioni di cui ho bisogno: controllare il calendario, aggiungere l'evento, prenotare la corsa e inviare un messaggio, tutto in una volta.
Oggi condivideremo maggiori informazioni sui piani di Android in questo ambito e su come voi, in qualità di sviluppatori, potete creare esperienze di prim'ordine.
Dan.
Grazie, Sarah.
Quindi, Android continua ad aggiungere l'AI al suo core, offrendo nuovi modi per preparare le tue app per questa era dell'AI.
Il primo di questi due pilastri dell'intelligence per gli sviluppatori è l'intelligence in-app, che ovviamente rende le tue app più smart, introducendo l'intelligence nella tua app. Puoi utilizzare modelli on-device come Gemini Nano 4 tramite ML Kit per riassumere i thread di messaggi, riscrivere le risposte e comprendere le immagini o utilizzare il linguaggio naturale per chiamare le funzioni della tua app.
Puoi anche utilizzare i modelli Gemini nel cloud direttamente nella tua app tramite Firebase AI Logic.
Oggi, però, ci concentreremo sul secondo pilastro, ovvero garantire che le funzionalità della tua app siano disponibili all'interno del sistema di intelligence.
Quindi, cos'è un sistema di intelligence?
Quindi, oltre a parlare e ospitare le esperienze con prompt di cui ha parlato Sarah in precedenza, è un sistema che consente integrazioni profonde per migliorare le esperienze nell'ecosistema delle app.
Pertanto, i dispositivi progettati per Gemini Intelligence, ad esempio, sfruttano questi punti di integrazione per fornire elementi come suggerimenti, notifiche riassunte, input vocale migliorato e compilazione automatica dei moduli.
Ora i suggerimenti fanno parte di una funzionalità chiamata Assistenza Proattiva.
Finora abbiamo parlato di assistenza su richiesta.
Siamo tutti abituati a questo, in cui un utente dice a un agente di eseguire una o più attività per suo conto.
Ma l'assistenza proattiva consiste in realtà nell'anticipare l'azione o le azioni successive che un utente potrebbe intraprendere.
Quindi, tornando al nostro esempio della cena, l'agente potrebbe mostrare in modo proattivo all'utente dei suggerimenti in risposta al messaggio, che potrebbero aggiungere l'invito al calendario o prenotare un servizio di car sharing.
Ma come fa effettivamente a chiedere l'intervento dell'utente?
L'assistenza proattiva può mostrare azioni in luoghi come la schermata di blocco, le notifiche, le tastiere virtuali e persino all'interno di app selezionate.
Quindi, come puoi fare in modo che la tua app venga effettivamente visualizzata in questi chip proattivi per indirizzare il traffico verso di te?
Beh, stai già facendo cose come creare l'app con l'ultima versione dell'SDK e con i nostri strumenti più moderni, ma la cosa migliore che puoi fare ora è assicurarti che la tua app sia pronta anche per l'assistenza con prompt, di cui Sarah parlerà tra poco.
Grazie, Dan.
Ok, quindi l'assistenza con prompt prevede che gli utenti si rivolgano direttamente a un agente e gli chiedano di avviare e orchestrare le attività, anche tra app.
In questo modo, è possibile automatizzare flussi di lavoro che in precedenza erano manuali e richiedevano molto tempo.
Per il nostro sistema di intelligence su Android, ci concentriamo principalmente su agenti di sistema come Gemini, che possono accedere alle app sul dispositivo dell'utente.
Esistono diversi modi in cui la tua app può essere visualizzata in questa esperienza e interagire con l'agente di sistema.
Esiste la nostra soluzione di automazione delle attività basata sulla piattaforma e poi ci sono le integrazioni API dirette nell'agente di sistema.
Consigliamo sempre di creare integrazioni API dirette, ove possibile.
Questi sono controllati dallo sviluppatore, quindi puoi decidere e controllare l'aspetto della tua app e del tuo brand.
Inoltre, sono più veloci e affidabili e offrono un'esperienza di qualità superiore ai tuoi utenti.
Esistono due percorsi API che puoi prendere in considerazione per l'integrazione con l'agente.
Il Model Context Protocol, o MCP, è la best practice del settore per la chiamata di funzione lato server.
Per quanto riguarda la chiamata degli strumenti on-device, annunciamo ufficialmente Android MCP, il framework di chiamata di funzione unificato su Android.
Android MCP è basato sulla nostra libreria Jetpack AppFunctions, che si allinea strettamente, sia dal punto di vista tecnico che concettuale, a MCP, supportando al contempo un'implementazione Kotlin familiare e idiomatica con cui molti sviluppatori Android hanno familiarità.
In questo modo, l'implementazione dell'API è relativamente intuitiva e facile da usare e puoi sfruttare le conoscenze acquisite da un'integrazione MCP lato server già configurata.
Molte primitive MCP vengono mappate one-to-one a un costrutto AppFunctions di Jetpack.
Vediamo alcuni esempi per illustrare questo concetto.
Un endpoint dello strumento MCP si mappa perfettamente a una funzione Kotlin annotata con AppFunctionDeclaration.
I modelli pydantic vengono mappati alle classi di dati Kotlin annotate con AppFunctionSerializable.
Tuttavia, anziché utilizzare il trasporto HTTP trasmissibile in streaming e/o l'I/O standard, Android MCP viene eseguito sull'IPC Android Binder ottimizzato.
Oggi siamo felici di annunciare che la libreria Jetpack, AppFunctions, è ora in versione beta.
Ok, probabilmente ti starai chiedendo quando ha senso utilizzare un percorso API e quando ha senso utilizzare Android MCP, nello specifico.
In linea di principio, Android MCP è particolarmente utile quando vuoi sfruttare il contesto on-device o Local Context per completare un percorso dell'utente.
Vediamo alcuni esempi in cui Android MCP dà il meglio di sé.
In particolare, dovresti prendere in considerazione Android MCP quando devi accedere a dati locali criptati.
Questo è particolarmente importante per le app di messaggistica e comunicazione, in cui la cronologia dei messaggi deve rimanere sul dispositivo a causa della crittografia end-to-end.
L'MCP Android è utile anche per le azioni puramente on-device e offline, se gli agenti le supportano.
Il nostro partner Wear OS, Samsung Health, ha lanciato una funzionalità per iniziare una corsa e avviare gli esercizi, orchestrati interamente on-device.
Gli utenti spesso vogliono che questi tipi di azioni vengano eseguite molto rapidamente, con un'elevata affidabilità, una bassa latenza e, a volte, anche in ambienti con connettività ridotta. Assicurarsi che l'intera chiamata allo strumento, il routing e il routing delle query possano avvenire on-device garantisce questo risultato.
MCP per Android è anche il percorso API preferito se vuoi accedere ai sensori del dispositivo o interagire direttamente con l'hardware del dispositivo.
Ad esempio, molte app per auto vogliono consentire agli utenti di aprire le portiere dell'auto istantaneamente tramite la connettività UWB e spesso in vivavoce.
Android MCP può essere d'aiuto in questo senso.
Infine, è la soluzione ideale se non è disponibile un endpoint cloud per le tue app o API.
Molte delle app proprietarie dei produttori di dispositivi sono solo lato client, quindi utilizzano Android MCP per questo motivo.
Ad esempio, Gemini può essere utilizzato per cercare e recuperare foto da app di galleria solo lato client, come Xiaomi Gallery.
Qui posso aprire Gemini e chiedere: "Trova le foto del bot Android che ho scattato la settimana scorsa nella Galleria di Xiaomi".
Xiaomi ha esposto una funzionalità dell'app per cercare foto, che Gemini chiama, e le foto pertinenti vengono restituite e mostrate direttamente nell'interfaccia utente dell'app Gemini, in modo che l'utente non debba mai uscire.
Inoltre, ci affidiamo alla stessa identica architettura per le azioni on-device nel sistema operativo Android.
Android MCP supporta la funzionalità di assistenza del dispositivo lanciata di recente, che è un'integrazione diretta del sistema operativo Android nell'app Gemini.
Questa funzionalità è già stata lanciata sui dispositivi Pixel e Lenovo ed è attualmente in fase di scale out nell'ecosistema OEM Android più ampio.
Gli utenti possono utilizzare l'assistenza del dispositivo andando su Gemini e chiedendo come possono ottimizzare le impostazioni, le impostazioni del dispositivo e le impostazioni di sistema per migliorare l'esperienza con il dispositivo.
Ad esempio, invece di cercare tra i vari menu, un utente daltonico può chiedere a Gemini: "Sono daltonico.
Come posso adattare questo smartphone alle mie esigenze?"
Gemini chiamerà e richiamerà le funzioni dell'app locale, ragionerà in modo intelligente sulla richiesta e suggerirà modi reali in cui l'utente può modificare le impostazioni di sistema e modi specifici in cui può modificare le impostazioni del dispositivo per migliorare l'esperienza.
Inoltre, i controlli in-app vengono visualizzati direttamente nell'interfaccia utente dell'app Gemini, in modo che gli utenti possano assumere immediatamente il controllo.
Ad esempio, per soddisfare questa richiesta di ottimizzazione per il daltonismo, Gemini ha mostrato questo pulsante di attivazione/disattivazione della correzione del colore, in modo che gli utenti possano apportare la modifica al volo.
Le esperienze MCP di Android sono già disponibili su alcuni dispositivi e stiamo collaborando con diversi partner per portare ancora più casi d'uso nell'ecosistema Android.
Sebbene le dichiarazioni esplicite degli strumenti MCP di Android siano il nostro standard di riferimento, sappiamo che ci saranno casi in cui gli utenti chiederanno agli agenti di completare casi d'uso complessi o meno comuni, ed è qui che entra in gioco la nostra funzionalità di automazione delle attività basata sulla piattaforma.
In questo modello, l'agente precaricato dall'OEM può utilizzare le API di controllo del computer di Android per simulare eventi touch sulla superficie di un'app.
Ciò non richiede modifiche al codice da parte degli sviluppatori e richiede che l'agente tocchi l'interfaccia utente come farebbe un utente, analizzando la schermata dell'app.
Per garantire il controllo e la sicurezza degli utenti, è fondamentale che gli agenti si mettano in pausa in corrispondenza di azioni potenzialmente sensibili, come i flussi di pagamento, e richiedano la conferma dell'utente per procedere.
Vediamo un esempio in azione.
In questo caso, un utente ha scattato una foto di un tour del caffè e del cioccolato e vuole chiedere a Expedia, o chiedere a Gemini di cercare su Expedia, se è possibile trovare un tour simile, ma con un numero di partecipanti pari a sei.
Senza bisogno di un'integrazione diretta dell'API, Gemini naviga nell'interfaccia utente di Expedia in background nello stesso modo in cui lo farebbe un utente.
Poi, mostra le opzioni pertinenti che ha trovato e le passa senza problemi all'utente quando è il momento della conferma finale e della prenotazione.
È importante capire che questi tre diversi percorsi non si escludono a vicenda e, anzi, sono più efficaci se combinati.
Utenti e sviluppatori dovrebbero aspettarsi che gli agenti siano in grado di coordinare gli strumenti tra le implementazioni dei server e anche l'app per Android locale per offrire agli utenti l'esperienza più completa e olistica possibile.
Okay.
Grazie, Sarah.
Approfondiremo un po' di più la piattaforma Android MCP.
E, quando possibile, voglio che tu sappia che sto estraendo gli snippet di codice MCP da Jetpacker, la nostra app di esempio per i viaggi, che è stata creata con l'aiuto di Antigravity.
L'hai visto nel keynote.
È dotato di ogni tipo di funzionalità intelligente, tra cui integrazioni on-device con le API di AI generativa e di riconoscimento vocale di ML Kit.
Ora l'MCP Android e l'MCP basato su cloud hanno una struttura simile.
Entrambi hanno client e server.
Nel mondo di MCP, i server forniscono strumenti e i client possono scoprirli e chiamarli.
Ora, con Android MCP, la tua app è il server e l'agente è il client.
La piattaforma Android funge da registro degli strumenti.
Ora le app iniziano registrando un servizio per fornire il server e noi inizieremo aggiungendo le dipendenze della libreria Jetpack AppFunctions nel nostro esempio qui.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)Poi utilizzeremo le annotazioni KSP per creare un AppFunctionService.
Questo servizio consente alle funzioni dell'app di rispettare correttamente i cicli di vita di Android.
Possiamo utilizzare l'inserimento delle dipendenze con Hilt per inserire repository di dati che le nostre AppFunction possono utilizzare.
@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
...Mi dispiace.
Torniamo al nostro diagramma.
Dopo aver configurato il servizio per il nostro server MCP Android, l'app registra gli strumenti sotto forma di AppFunctions, insieme ai modelli di dati associati utilizzati.
Ora puoi registrare un modello di dati utilizzando l'annotazione AppFunctionSerializable e questo KDoc è davvero interessante.
/** 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,
)Diventeranno semplicemente metadati utilizzati dall'agente per capire come utilizzare il modello.
Puoi utilizzare l'annotazione AppFunctionDeclaration per registrare la tua funzione come strumento.
@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) }
}
}Con isDescribedByKDoc impostato su true, KDoc fornisce all'agente una descrizione per capire cosa fa la tua funzione e aiuta anche gli esseri umani a comprenderla.
/**
* 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()
...Ecco alcune best practice per la progettazione dei parametri.
/**
* @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)Innanzitutto, devi concentrarti sul sottoinsieme minimo di parametri con nomi chiari e distinti.
Rendi i parametri non essenziali nullable e risolvi i relativi valori predefiniti nel corpo della funzione.
Non si tratta solo di una best practice.
Non funzionerà, perché i valori predefiniti di Kotlin verranno ignorati in quanto la libreria AppFunctions passa null per i parametri nullable omessi.
Non mostrerò nemmeno un esempio di questo primo caso, ma non utilizzare parametri generici.
Cose come il contesto completo o la cronologia delle conversazioni sono semplicemente pessime.
Possono indurre gli agenti a trasmettere grandi quantità di dati e portare all'esfiltrazione, oltre a essere una cattiva pratica.
Poiché l'AppFunction viene eseguita per impostazione predefinita sul thread dell'interfaccia utente, devi assicurarti che qualsiasi operazione a lunga esecuzione avvenga in background scegliendo il coroutine dispatcher appropriato.
@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 }
...Quando necessario, devi generare eccezioni AppFunction con informazioni dettagliate in modo che l'agente possa capire come risolvere un errore.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}Bene, ora abbiamo una funzione AppFunction, torniamo al nostro diagramma dell'architettura.
L'app client dell'agente si registra con la piattaforma per chiamare le AppFunctions.
Ad esempio, Gemini.
Per farlo, devono disporre di un'autorizzazione con privilegi. Gli agenti registrati possono quindi cercare i metadati di AppFunction dalla piattaforma Android e decidere se eseguire una o più AppFunction registrate per soddisfare la richiesta di un utente.
A proposito di una o più AppFunction, come fa l'agente a decidere?
Un'ultima cosa che devi fare è descrivere le funzionalità della tua app in un file di metadati dell'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" />Gli agenti selezionano gli strumenti leggendo prima questa descrizione all'interno del tuo AppFunctionAppMetadata e poi leggendo le descrizioni con le singole AppFunction.
Pertanto, ti consigliamo di concentrare l'inizio della descrizione su una breve spiegazione del dominio principale della tua app o dei tuoi strumenti.
Puoi far seguire a questo i pattern operativi per aiutare l'agente a comprendere il flusso logico delle operazioni. Questo è utile per delineare i prerequisiti o indirizzare l'agente verso i percorsi ottimali per il completamento delle attività.
Inoltre, puoi aggiungere una sezione sui vincoli che definisce i limiti logici dei tipi di dati di input o rafforza i pattern operativi.
Queste sono le nozioni di base di Android MCP, ma ci sono altre cose che puoi fare.
Una cosa interessante che puoi fare è attivare e disattivare AppFunctions in modo dinamico.
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}Questo è importante, ad esempio, se vuoi che AppFunction sia disponibile solo se l'utente ha eseguito l'accesso.
Un'altra cosa che puoi fare è creare AppFunctions con ambito limitato a una schermata o un'attività specifica della tua 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
}Quindi, invece di dichiarare AppFunction, dichiari AppFunctionSignature.
Puoi quindi implementare un gestore per AppFunction nella tua UI.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}La cosa interessante è che il ciclo di vita dei gestori è automaticamente legato al ciclo di vita dell'ambito della coroutine che utilizzi.
Ora dovrai memorizzare nella cache questo adattatore di richieste AppFunctions, scusa, l'adattatore di richieste AppFunctions, poiché la libreria utilizza la riflessione in background per crearne un'istanza.
Inoltre, è disponibile una funzionalità per creare AppFunction con ambito la cui registrazione non è direttamente legata al ciclo di vita di Android.
Un'ultima cosa: abbiamo un elemento chiamato livello di accesso.
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...Puoi ottenere e utilizzare il livello di accesso self, system o trusted, che indica quali funzioni sono destinate esclusivamente all'uso per il tuo agente in-app, ad esempio uno creato con ML Kit GenAI, quali funzioni sono disponibili solo per il sistema e quali funzioni sono disponibili per gli agenti di sistema attendibili.
Per aiutarti a creare il tuo agente in-app, abbiamo già annunciato che ML Kit supporta in modo nativo AppFunctions come parte del suo framework unificato di chiamata degli strumenti.
Le definisci una volta e ottieni la copertura sia degli agenti a livello di sistema sia dei modelli locali in-app, il che ti consente di evitare dichiarazioni di strumenti frammentate.
Per saperne di più, puoi guardare questo talk, Unlock agentic capabilities in your app with ML Kit GenAI, o semplicemente andare a vedere le ultime novità di Jetpack.
È piuttosto interessante.
Ok, torniamo a Sara.
Okay.
Dan ti ha appena mostrato come funziona Android MCP dietro le quinte.
Ora mettiamola in pratica.
Ricorda che la tua app è un server e gli agenti sono il client, quindi l'obiettivo deve essere quello di assicurarti che la tua app sia un buon server per loro.
Seguiremo Jetpacker, la nostra app di esempio per i viaggi, dalla prima dichiarazione dello strumento alla chiamata di funzione effettiva tramite Gemini.
Puoi scrivere le tue AppFunction manualmente o utilizzare un agente di programmazione per iniziare.
Abbiamo rilasciato la nostra skill AppFunctions che può aiutarti a iniziare a scrivere le tue funzioni, che puoi collegare ad Android Studio tramite la CLI Android o utilizzare con l'agente di programmazione che preferisci.
android skills add appfunctionsLa skill funziona in tre passaggi.
Innanzitutto, analizza il codebase per identificare i percorsi dell'utente principali che potrebbero essere utili, come la riproduzione di contenuti multimediali, la creazione di una nota o l'invio di un messaggio.
Quindi, genera, genera effettivamente dichiarazioni AppFunction che espongono questi percorsi completi dell'implementazione e del supporto della piattaforma di cui hanno bisogno.
Ad esempio, Gradle e KSP, XML dei metadati dell'app e l'inserimento delle dipendenze.
Infine, perfezionerà le descrizioni KDoc, perché è esattamente ciò che l'agente utilizza per decidere quando e come chiamare la funzione.
Ecco il tipo di risultato che stiamo cercando.
Questa è una funzione addExpense per 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 descrizione è una frase di una riga che inizia con un verbo e una risorsa: aggiunge una spesa a un viaggio.
I campi trip e category sono nullable perché gli utenti potrebbero non specificarli sempre, ma la funzione li risolverà nel corpo.
Quando una richiesta non può essere eseguita, genera un AppFunctionException in modo che l'agente sappia cosa è andato storto.
Ora passiamo ai test.
Ora le tue funzioni sono servizi che gli agenti possono utilizzare.
In realtà, dovrai testare due parti: il comportamento della funzione e il comportamento dell'agente.
Per la tua funzione, inizieremo con il test delle unità che probabilmente hai già scritto, ma ci sono un paio di cose da tenere a mente quando è l'agente a eseguire effettivamente la chiamata di funzione.
Innanzitutto, i valori predefiniti di Kotlin non si applicano in questo caso.
Un parametro omesso viene visualizzato come null, false o 0.
Quindi, un limite che in precedenza era impostato su 10 per impostazione predefinita, qui sarà pari a 0.
Inoltre, la chiamata inizia sul thread principale e la chiamata della funzione da un test potrebbe non renderlo sempre evidente, quindi dovresti eseguire l'IO standard su un dispatcher in background.
Quando si verifica un errore, genera un AppFunctionException.
Qualsiasi altro errore verrà inviato all'agente come errore sconosciuto generico con il messaggio di eccezione non elaborato allegato.
Inoltre, gli input proverranno da un modello, quindi dovresti assolutamente testare scenari di casi limite come spazi vuoti, sinonimi di dollari come USD e valori fuori intervallo.
Per quanto riguarda l'agente, devi sapere se sceglie lo strumento al momento giusto e se esegue la funzione correttamente.
Ogni agente si comporta in modo leggermente diverso, quindi è importante testare le funzioni con ogni agente che prevedi di utilizzare.
Puoi utilizzare il nostro agente di esempio open source per eseguire i test o puoi utilizzare Gemini per testare questa integrazione.
Se vuoi vedere come Gemini interagisce con le tue AppFunctions o se vuoi lanciare un MCP per Android nell'app Gemini, puoi farlo con la Connected Apps Platform, che abbiamo lanciato oggi ed è attualmente in anteprima.
Per saperne di più, dai un'occhiata al nostro precedente intervento sull'estensione della tua app a Gemini e alla Ricerca tramite la Connected Apps Platform.
Una volta che fai parte del programma di anteprima, la procedura per eseguire i test con Gemini prevede due passaggi.
Per prima cosa, creeremo un'app connessa in Google Connected Console che Gemini può effettivamente utilizzare.
In secondo luogo, in Play Console, dovremo caricare l'app bundle, in modo da poter accedere alle AppFunctions, e potrai iniziare su un canale di test interno, in modo che i tester possano effettivamente convalidare le funzioni.
Iniziamo con Google Connected Console.
Qui creerai un'app connessa utilizzando il pulsante "Crea un'app" in alto a destra.
Quando crei l'app connessa, scegli Android MCP in questa schermata.
Se stessi creando un'implementazione MCP lato server, qui caricheresti il codice del server.
Quando fai clic su "Configura", tutte le app Google Play associate al tuo account sviluppatore verranno visualizzate qui, pronte per essere collegate alla tua app connessa.
Se non hai ancora un'app Google Play, devi prima crearne una in Play Console.
In questo caso, abbiamo già Jetpacker, quindi procederemo con il collegamento.
Successivamente, aggiungi i dettagli che gli utenti vedranno nel frontend di Gemini quando proveranno a interagire con la tua app.
Sono inclusi elementi come l'icona dell'app, il nome visualizzato, le funzionalità della tua app connessa e i prompt di esempio che gli utenti possono utilizzare per interagire con l'app.
Questi devono essere descrittivi per aiutare al meglio gli utenti.
Ok, questo conclude la configurazione dell'app connessa, ma potresti chiederti come l'app connessa ottiene effettivamente l'accesso alle funzioni.
Poiché le funzioni sono contenute nel codice dell'app, ora dobbiamo andare su Play Console, caricare l'app bundle e creare un canale di test, in modo da poterlo testare con utenti reali.
In questa schermata, caricheremo l'app bundle e vedremo che Play è in grado di rilevare le AppFunctions, che potrai collegare direttamente alla tua app connessa.
Ora possiamo vedere l'app connessa Jetpack completa con le tue funzioni integrate.
Successivamente, dovremo aggiungere dei tester, in modo che ci sia qualcuno pronto a convalidare le tue funzioni e ad assicurarsi che funzionino correttamente.
Quindi, fai clic sulle versioni dell'app connessa qui.
In questa schermata, puoi selezionare "Gestisci i tester" e configurare un elenco limitato di tester che potranno accedere alla tua app connessa in Gemini una volta che sarà disponibile.
Puoi aggiungere tester a questo canale di test nello stesso modo in cui lo faresti per qualsiasi altra release.
Assicurati di avere lo stesso elenco di tester per l'app connessa e per l'app Google Play.
Ora testiamo l'integrazione.
Ecco l'app Jetpacker sullo smartphone di un utente, con un viaggio già registrato per questo summit.
Aprirò Gemini per collegare l'app.
Devo assicurarmi di avere installato sul mio dispositivo la stessa versione dell'app che abbiamo caricato in precedenza su Play e di utilizzare un account per Gemini che è stato aggiunto all'elenco dei tester.
Quindi, menzionerò Jetpacker e chiederò il numero di conferma della mia camera d'hotel a Mountain View.
Gemini chiama le AppFunctions di Jetpacker sul dispositivo e risponde: "RED11223 al Redwood Grove Hotel".
Ora chiederò: "Quanto budget mi rimane per ADS?"
La chiamata di funzione restituirà 1850 $.
Ora voglio apportare una modifica, quindi chiederò a Gemini di aggiungere 45 $per l'Uber per l'aeroporto al mio viaggio ADS.
Gemini chiama la nostra funzione addExpense di prima per aggiungere l'attività direttamente al mio itinerario.
Quando apro Jetpacker, la spesa è già presente e il budget è stato ridotto di conseguenza.
Se vuoi iniziare a creare app connesse per Gemini oggi stesso, richiedi l'accesso alla piattaforma Connected Apps, attualmente in anteprima.
Okay.
Quindi, concludiamo.
Siamo quindi all'inizio di un enorme cambiamento nell'informatica mobile ed è davvero il momento di assicurarti che la tua app sia pronta.
Quindi, inizia identificando i percorsi degli utenti di alto valore principali della tua app.
Esponi il backend del tuo cloud utilizzando l'MCP lato server e utilizza l'MCP Android per lavorare con dati criptati end-to-end, operazioni locali, sensori del dispositivo e altri elementi on-device.
Per creare un prototipo, puoi scaricare Android Studio e la CLI Android.
Puoi aggiungere la dipendenza beta di AndroidX AppFunctions ai file di build e testare localmente la tua prima funzione on-device.
Per registrarti e iniziare in anticipo a utilizzare Gemini in produzione, puoi registrarti tramite questo codice QR.
E, naturalmente, non dimenticare di dare un'occhiata a questi interventi correlati sull'utilizzo della Connected Apps Platform e su come puoi utilizzare le AppFunctions interne con ML Kit o con il tuo agente per gestire il tuo agente in-app.
Infine, grazie per il tempo che ci hai dedicato.
Non vediamo l'ora di scoprire tutte le fantastiche esperienze che stai creando per Android.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)@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
.../** 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,
)@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) }
}
}/**
* 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()
.../**
* @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)@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 }
...@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}<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" />viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}@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
}val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...android skills add appfunctions/**
* 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))
}