Menedżer produktu w Google
Android staje się systemem inteligentnym, w którym agenty takie jak Gemini koordynują zadania w różnych aplikacjach. Aby zapewnić użytkownikom nowe wrażenia i umożliwić im korzystanie z tych przepływów pracy opartych na agentach, aplikacja musi udostępniać swoje podstawowe funkcje.
Ta sesja to praktyczny przewodnik po integracji aplikacji z warstwą AI Androida. Przeanalizujemy kluczowe ścieżki integracji, w tym MCP na urządzeniu z Androidem, MCP po stronie serwera i automatyzację zadań, oraz przedstawimy ramy decyzyjne, które pomogą Ci wybrać odpowiednią ścieżkę w Twoim przypadku. Omówimy też proaktywne funkcje spersonalizowanego kontekstu.
Zapoznasz się z tymi zagadnieniami:
AppFunctionDeclaration, podaj opisy KDoc i unikaj polegania na wartościach domyślnych Kotlina.Transkrypcja
OK.
Cześć!
Nazywam się Dan Galpin i pracuję w zespole ds. relacji z deweloperami.
A ja nazywam się Sarah Shamsie i jestem menedżerką produktu pracującą nad platformami analizy deweloperskiej na Androida.
Bardzo się cieszymy, że dołączysz do dzisiejszej sesji.
Obserwujemy zmianę paradygmatu w sposobie interakcji użytkowników z urządzeniami.
W erze internetu użytkownicy zwykle zaczynali od otwarcia przeglądarki, a następnie korzystali z funkcji wyszukiwania.
W erze urządzeń mobilnych zachowania użytkowników zmieniły się i zaczęli oni korzystać z bardziej wyspecjalizowanych aplikacji natywnych do wykonywania jednorazowych działań.
Oto przykład:
Otrzymuję wiadomość od znajomego z zaproszeniem na kolację.
Żeby odpowiedzieć, muszę sprawdzić w kalendarzu, czy mam wolny czas, dodać wydarzenie do kalendarza, zarezerwować przejazd, jeśli potrzebuję transportu, i odpisać znajomemu, że mogę przyjść.
Wcześniej wymagało to przełączania się między różnymi aplikacjami na urządzeniu, aby wykonać działania i zrealizować podróż.
Teraz jednak żyjemy w erze inteligencji.
W erze inteligencji użytkownicy zaczynają swoje ścieżki od wyrażenia ogólnych zamiarów i celów, a nie konkretnych działań, które chcą wykonać.
Nastąpiła fundamentalna zmiana – użytkownicy postrzegają swoje urządzenia jako coś, co aktywnie przewiduje ich potrzeby i pomaga im osiągać cele, a nie tylko umożliwia wykonywanie działań.
Zauważyliśmy, że oczekują, że ten system AI będzie działać nie tylko na urządzeniach mobilnych, ale też w całym ekosystemie urządzeń, w tym na zegarkach, w samochodach, na telewizorach, laptopach, urządzeniach XR i okularach.
W erze AI jeden prompt obejmujący wiele aplikacji może ułatwić wysłanie zaproszenia na kolację, o którym wspominaliśmy wcześniej.
Wystarczy jeden prompt, aby agent na moim urządzeniu pomógł mi wykonać wszystkie potrzebne działania – od sprawdzenia kalendarza, przez dodanie wydarzenia i zamówienie przejazdu, po wysłanie wiadomości.
Dzisiaj opowiemy więcej o planach Androida w tym zakresie i o tym, jak deweloperzy mogą tworzyć najwyższej jakości rozwiązania.
Dan.
Dziękuję, Sarah.
Android stale dodaje inteligentne funkcje, co daje nowe możliwości aplikacjom w tej erze inteligencji.
Pierwszym z tych 2 filarów jest inteligencja w aplikacji, która oczywiście sprawia, że aplikacje są mądrzejsze. Możesz używać modeli na urządzeniu, takich jak Gemini Nano 4, za pomocą ML Kit, aby podsumowywać wątki wiadomości, przepisywać odpowiedzi i rozumieć obrazy lub używać języka naturalnego do wywoływania funkcji aplikacji.
Możesz nawet używać modeli Gemini w chmurze bezpośrednio w aplikacji za pomocą Firebase AI Logic.
Dzisiaj skupimy się jednak na drugim filarze, czyli na zapewnieniu, że możliwości Twojej aplikacji są dostępne w systemie AI.
Czym jest system oparty na sztucznej inteligencji?
Oprócz rozmów i obsługi tych funkcji, o których Sarah mówiła wcześniej, jest to system, który umożliwia głęboką integrację w celu ulepszenia działania całego ekosystemu aplikacji.
Na przykład urządzenia zaprojektowane pod kątem Gemini Intelligence wykorzystują te punkty integracji, aby udostępniać takie funkcje jak sugestie, podsumowania powiadomień, ulepszone wprowadzanie głosowe i automatyczne wypełnianie formularzy.
Teraz sugestie są częścią funkcji o nazwie proaktywna pomoc.
Do tej pory mówiliśmy o pomocy na żądanie.
Wszyscy jesteśmy przyzwyczajeni do tego, że użytkownik prosi agenta o wykonanie jednego lub kilku zadań w jego imieniu.
Proaktywna pomoc polega jednak na przewidywaniu kolejnych działań, które użytkownik może podjąć.
Wracając do naszego przykładu z kolacją, agent może proaktywnie wyświetlić użytkownikowi sugestie w odpowiedzi na wiadomość, które mogą dodać zaproszenie do kalendarza lub zarezerwować przejazd.
Jak jednak wygląda prośba o zgodę?
Proaktywna pomoc może wyświetlać działania w takich miejscach jak ekran blokady, powiadomienia, klawiatury wirtualne, a nawet w wybranych aplikacjach.
Co zrobić, aby aplikacja pojawiała się w tych proaktywnych elementach i przyciągała ruch?
Well, you're already doing things like building against the latest SDK version and building with our most modern tools, but the best thing you can do now is to make sure that your app is also ready for prompted assistance, which Sarah will talk about next.
Dziękuję, Dan.
Pomoc na podstawie promptów polega na tym, że użytkownicy zwracają się bezpośrednio do agenta i proszą go o rozpoczęcie i koordynowanie zadań, nawet w różnych aplikacjach.
Może to zautomatyzować przepływy pracy, które wcześniej były wykonywane ręcznie i były czasochłonne.
W przypadku naszego systemu inteligentnego na Androidzie skupiamy się przede wszystkim na agentach systemowych, takich jak Gemini, które mogą korzystać z aplikacji na urządzeniu użytkownika.
Istnieje kilka różnych sposobów, na jakie Twoja aplikacja może się wyświetlać w tym środowisku i wchodzić w interakcje z agentem systemowym.
Mamy rozwiązanie do automatyzacji zadań oparte na platformie oraz bezpośrednie integracje interfejsu API z agentem systemowym.
Zawsze zalecamy tworzenie bezpośrednich integracji z interfejsem API, jeśli tylko jest to możliwe.
Są one kontrolowane przez dewelopera, więc możesz decydować o tym, jak będzie wyglądać Twoja aplikacja i jak będzie prezentowana Twoja marka.
Są też szybsze i bardziej niezawodne, co zapewnia użytkownikom lepsze wrażenia.
Jeśli chcesz zintegrować się z agentem, możesz rozważyć 2 ścieżki interfejsu API.
Model Context Protocol (MCP) to najlepsza praktyka branżowa dotycząca wywoływania funkcji po stronie serwera.
Jeśli chodzi o wywoływanie narzędzi na urządzeniu, oficjalnie ogłaszamy Android MCP, ujednoliconą platformę wywoływania funkcji na Androidzie.
Android MCP korzysta z naszej biblioteki Jetpack AppFunctions, która jest ściśle powiązana z MCP zarówno pod względem technicznym, jak i koncepcyjnym, a jednocześnie obsługuje znaną i idiomatyczną implementację Kotlina, którą zna wielu deweloperów aplikacji na Androida.
Dzięki temu wdrożenie interfejsu API jest stosunkowo intuicyjne i łatwe, a Ty możesz wykorzystać wiedzę zdobytą podczas integracji MCP po stronie serwera.
Wiele elementów podstawowych MCP jest mapowanych bezpośrednio na konstrukcję AppFunctions w Jetpacku.
Przyjrzyjmy się kilku przykładom.
Punkt końcowy narzędzia MCP dobrze pasuje do funkcji Kotlin z adnotacją AppFunctionDeclaration.
Twoje modele pydantic są mapowane na klasy danych Kotlin z adnotacją AppFunctionSerializable.
Zamiast strumieniowego przesyłania HTTP lub standardowego transportu wejścia/wyjścia Android MCP działa jednak na zoptymalizowanym protokole Android Binder IPC.
Z przyjemnością informujemy, że biblioteka Jetpack AppFunctions jest teraz dostępna w wersji beta.
Pewnie zastanawiasz się, kiedy warto używać jednej ścieżki interfejsu API, a kiedy Android MCP.
Ogólna zasada jest taka, że Android MCP jest szczególnie przydatny, gdy chcesz wykorzystać kontekst na urządzeniu lub kontekst lokalny, aby zrealizować ścieżkę użytkownika.
Przyjrzyjmy się kilku przykładom, w których Android MCP sprawdza się znakomicie.
Warto rozważyć użycie Android MCP, gdy potrzebujesz dostępu do lokalnych, zaszyfrowanych danych.
Jest to szczególnie ważne w przypadku aplikacji do obsługi wiadomości i komunikacji, w których historia wiadomości musi pozostać na urządzeniu ze względu na pełne szyfrowanie.
Android MCP jest też przydatny w przypadku działań wykonywanych wyłącznie na urządzeniu i offline, jeśli agenci je obsługują.
Nasz partner Wear OS, Samsung Health, wprowadził funkcję rozpoczynania biegu i ćwiczeń, która jest w pełni obsługiwana na urządzeniu.
Użytkownicy często chcą, aby te działania były wykonywane bardzo szybko, z wysoką niezawodnością i niskim opóźnieniem, a czasami nawet w środowiskach o niskiej jakości połączenia. Możliwość wywoływania narzędzi i kierowania zapytań na urządzeniu zapewnia taką możliwość.
Android MCP to również preferowana ścieżka interfejsu API, jeśli chcesz uzyskać dostęp do czujników urządzenia lub bezpośrednio korzystać z jego sprzętu.
Na przykład wiele aplikacji motoryzacyjnych umożliwia użytkownikom natychmiastowe odblokowywanie drzwi samochodu za pomocą łączności UWB, często obsługiwany bez użycia rąk.
W tym przypadku może pomóc Android MCP.
Jest to też rozwiązanie, jeśli nie ma dostępnego Cloud Endpoints dla Twoich aplikacji lub interfejsu API.
Wiele aplikacji własnych producentów urządzeń działa tylko po stronie klienta, dlatego używają oni Android MCP.
Gemini może na przykład wyszukiwać i pobierać zdjęcia z aplikacji galerii, które działają tylko po stronie klienta, takich jak Xiaomi Gallery.
Mogę otworzyć Gemini i poprosić: „Znajdź zdjęcia robota z Androidem, które zrobiłem w zeszłym tygodniu w aplikacji Xiaomi Gallery”.
Xiaomi udostępniło funkcję wyszukiwania zdjęć, którą Gemini następnie wywołuje, a odpowiednie zdjęcia są zwracane i wyświetlane bezpośrednio w interfejsie aplikacji Gemini, dzięki czemu użytkownik nie musi jej opuszczać.
W przypadku działań na urządzeniu w systemie operacyjnym Android korzystamy z tej samej architektury.
Android MCP obsługuje niedawno wprowadzoną funkcję pomocy na urządzeniu, która jest bezpośrednią integracją systemu operacyjnego Android z aplikacją Gemini.
Ta funkcja została już wprowadzona na urządzeniach Pixel i Lenovo, a obecnie jest skalowana w poziomie w szerszym ekosystemie producentów OEM urządzeń z Androidem.
Użytkownicy mogą korzystać z pomocy dotyczącej urządzenia, przechodząc do Gemini i pytając, jak mogą zoptymalizować ustawienia, ustawienia urządzenia i ustawienia systemowe, aby poprawić działanie urządzenia.
Na przykład zamiast szukać w różnych menu, użytkownik z daltonizmem może zapytać Gemini: „Jestem daltonistą.
Jak mogę lepiej korzystać z tego telefonu?”.
Gemini wywoła funkcje aplikacji lokalnej i inteligentnie przeanalizuje prośbę, a następnie zasugeruje konkretne sposoby zmiany ustawień systemu i ustawień urządzenia, aby poprawić działanie.
Następnie opcje zarządzania w aplikacji są wyświetlane bezpośrednio w interfejsie aplikacji Gemini, dzięki czemu użytkownicy mogą natychmiast przejąć kontrolę.
Na przykład w odpowiedzi na prośbę o optymalizację dla osób ze ślepotą barw Gemini wyświetlił przełącznik korekcji kolorów, dzięki czemu użytkownicy mogą wprowadzać zmiany na bieżąco.
Funkcje MCP na Androidzie są już dostępne na wybranych urządzeniach, a my współpracujemy z wieloma partnerami, aby wprowadzić jeszcze więcej zastosowań w ekosystemie Androida.
Chociaż deklaracje narzędzi MCP na Androida są naszym złotym standardem, wiemy, że będą zdarzać się sytuacje, w których użytkownicy będą prosić agentów o wykonanie złożonych lub mniej typowych zadań. W takich przypadkach przydaje się nasza funkcja automatyzacji zadań oparta na platformie.
W tym modelu wstępnie załadowany agent OEM może używać interfejsów API Androida do sterowania komputerem, aby symulować zdarzenia dotknięcia na powierzchni aplikacji.
Nie wymaga to od deweloperów żadnych zmian w kodzie. Agent musi klikać elementy interfejsu tak, jak robiłby to użytkownik, analizując ekran aplikacji.
Aby zapewnić użytkownikom kontrolę i bezpieczeństwo, agenci muszą wstrzymywać działania w przypadku potencjalnie poufnych operacji, takich jak procesy płatności, i prosić użytkownika o potwierdzenie, zanim przejdą dalej.
Przeanalizujmy to na przykładzie.
W tym przypadku użytkownik zrobił zdjęcie wycieczki połączonej z degustacją kawy i czekolady i chce poprosić Gemini o sprawdzenie na stronie Expedia, czy można znaleźć podobną wycieczkę, ale dla 6 osób.
Bez konieczności bezpośredniej integracji z interfejsem API Gemini porusza się po interfejsie Expedia w tle w taki sam sposób, jak użytkownik.
Następnie wyświetla znalezione odpowiednie opcje i płynnie przekazuje je użytkownikowi, gdy nadejdzie czas na ostateczne potwierdzenie i rezerwację.
Warto pamiętać, że te 3 różne ścieżki nie wykluczają się wzajemnie, a wręcz najlepiej działają razem.
Użytkownicy i deweloperzy powinni oczekiwać, że agenci będą mogli koordynować narzędzia w ramach implementacji serwerowych, a następnie lokalnych aplikacji na Androida, aby zapewnić użytkownikom jak najbardziej kompleksowe i holistyczne wrażenia.
OK.
Dziękuję, Sarah.
Przyjrzyjmy się bliżej platformie Android MCP.
Jeśli to możliwe, chcę, żebyś wiedział, że pobieram fragmenty kodu MCP z Jetpackera, czyli naszej przykładowej aplikacji podróżniczej, która została utworzona przy użyciu Antigravity.
Widzieliście to już podczas prezentacji.
Jest wyposażony we wszelkiego rodzaju przydatne funkcje inteligentne, w tym integracje na urządzeniu z interfejsami API ML Kit Gen AI i ML Kit do rozpoznawania mowy.
Teraz protokół MCP na Androidzie i protokół MCP działający w chmurze mają podobną strukturę.
Oba mają klientów i serwery.
W świecie MCP serwery udostępniają narzędzia, a klienty mogą je wykrywać i wywoływać.
Teraz dzięki Android MCP Twoja aplikacja jest serwerem, a agent – klientem.
Platforma Androida pełni rolę rejestru narzędzi.
Aplikacje zaczynają od zarejestrowania usługi, która udostępnia serwer. My zaczniemy od dodania zależności biblioteki Jetpack AppFunctions w naszym przykładzie.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)Następnie użyjemy adnotacji KSP, aby utworzyć AppFunctionService.
Dzięki tej usłudze funkcje aplikacji mogą prawidłowo uwzględniać cykle życia Androida.
Możemy użyć wstrzykiwania zależności za pomocą Hilt, aby wstrzyknąć repozytoria danych, których mogą używać nasze funkcje aplikacji.
@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
...Przepraszam.
Wróćmy do naszego diagramu.
Po skonfigurowaniu usługi na naszym serwerze MCP Android aplikacja rejestruje narzędzia w formie funkcji AppFunctions wraz z powiązanymi modelami danych, których używają.
Teraz możesz zarejestrować model danych za pomocą adnotacji AppFunctionSerializable, a ten KDoc jest całkiem fajny.
/** 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,
)Stanie się on po prostu metadanymi, które będą używane przez agenta do zrozumienia, jak korzystać z modelu.
Możesz użyć adnotacji AppFunctionDeclaration, aby zarejestrować funkcję jako narzędzie.
@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) }
}
}Gdy parametr isDescribedByKDoc jest ustawiony na wartość „true”, KDoc udostępnia agentowi opis, który pomaga mu zrozumieć, co robi funkcja. Pomaga to również ludziom.
/**
* 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()
...Oto kilka sprawdzonych metod projektowania parametrów.
/**
* @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)Przede wszystkim skup się na minimalnym podzbiorze parametrów o jasnych, odrębnych nazwach.
Ustaw parametry, które nie są niezbędne, jako dopuszczające wartość null i rozwiązuj ich wartości domyślne w treści funkcji.
To nie jest tylko sprawdzona metoda.
To nie zadziała, ponieważ wartości domyślne Kotlin zostaną zignorowane, ponieważ biblioteka AppFunctions przekazuje wartość null w przypadku pominiętych parametrów, które mogą mieć wartość null.
Nie będę nawet pokazywać przykładu tego pierwszego, ale nie używaj parametrów typu „catch-all”.
Takie rzeczy jak pełny kontekst czy historia rozmów są po prostu złe.
Mogą one prowadzić do przekazywania przez agentów dużych ilości danych i ich eksfiltracji, co jest po prostu niewłaściwą praktyką.
Ponieważ funkcja AppFunction jest domyślnie wykonywana w wątku interfejsu, musisz się upewnić, że każda długo trwająca operacja jest wykonywana w tle, wybierając odpowiedni dyspozytor współprogramu.
@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 }
...W razie potrzeby możesz zgłaszać wyjątki AppFunction ze szczegółowymi informacjami, aby agent wiedział, jak rozwiązać błąd.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}Mamy już AppFunction. Wróćmy do schematu architektury.
Aplikacja kliencka agenta rejestruje się na platformie, aby wywoływać funkcje AppFunctions.
Przykładem jest Gemini.
Aby to zrobić, muszą mieć uprawnienia uprzywilejowane. Zarejestrowani agenci mogą następnie wyszukiwać metadane AppFunction na platformie Androida i decydować, czy powinni wykonać co najmniej 1 zarejestrowaną funkcję AppFunction, aby spełnić prośbę użytkownika.
Jeśli chodzi o AppFunctions, jak agent podejmuje decyzję?
Ostatnią rzeczą, którą musisz zrobić, jest opisanie możliwości aplikacji w pliku metadanych aplikacji.
<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" />Agenci wybierają narzędzia, najpierw czytając ten opis w AppFunctionAppMetadata, a następnie czytając opisy poszczególnych funkcji aplikacji.
Dlatego na początku opisu warto krótko wyjaśnić, do czego służy aplikacja lub narzędzie.
Następnie możesz dodać wzorce operacyjne, aby pomóc agentowi zrozumieć logiczny przepływ operacji. Jest to przydatne do określania wymagań wstępnych lub kierowania agenta na optymalne ścieżki wykonywania zadań.
Możesz też dodać sekcję dotyczącą ograniczeń, która określa logiczne limity typów danych wejściowych lub wzmacnia wzorce operacyjne.
To były podstawy dotyczące MCP na Androidzie, ale możesz zrobić jeszcze kilka rzeczy.
Możesz dynamicznie włączać i wyłączać funkcje AppFunctions.
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}Jest to ważne, jeśli na przykład chcesz, aby funkcja AppFunction była dostępna tylko wtedy, gdy użytkownik jest zalogowany.
Możesz też utworzyć funkcje aplikacji, które są ograniczone do konkretnego ekranu lub aktywności w aplikacji.
@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
}Zamiast AppFunction deklarujesz AppFunctionSignature.
Następnie możesz zaimplementować moduł obsługi funkcji AppFunction w interfejsie.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}Fajne jest to, że cykl życia modułów obsługi jest automatycznie powiązany z cyklem życia używanego zakresu współprogramu.
Teraz musisz zapisać w pamięci podręcznej ten adapter żądań AppFunctions, ponieważ biblioteka używa refleksji, aby utworzyć jego instancję.
Dostępna jest też funkcja tworzenia funkcji aplikacji o ograniczonym zakresie, których rejestracja nie jest bezpośrednio powiązana z cyklem życia Androida.
Ostatnia rzecz to poziom dostępu.
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...Możesz uzyskać i używać poziomu dostępu „self”, „system” lub „trusted”. Wskazuje on, które funkcje są przeznaczone tylko do użytku przez Twojego agenta w aplikacji, np. utworzonego za pomocą ML Kit GenAI, które funkcje są dostępne tylko dla systemu, a które dla zaufanych agentów systemowych.
Aby pomóc Ci w tworzeniu własnego agenta w aplikacji, ogłosiliśmy już, że ML Kit natywnie obsługuje AppFunctions w ramach ujednoliconej platformy wywoływania narzędzi.
Definiujesz je raz i uzyskujesz zasięg zarówno w przypadku agentów na poziomie systemu, jak i lokalnych modeli w aplikacji, co pozwala uniknąć rozproszonych deklaracji narzędzi.
Aby dowiedzieć się więcej, możesz obejrzeć tę prezentację: „Odblokuj możliwości agentowe w swojej aplikacji dzięki ML Kit GenAI” lub po prostu sprawdzić, co nowego jest w Jetpacku.
To całkiem fajne.
Wróćmy do Sary.
OK.
Dan właśnie pokazał, jak działa Android MCP.
Teraz czas wykorzystać tę wiedzę.
Pamiętaj, że Twoja aplikacja jest serwerem, a agenci są klientami, więc celem powinno być zapewnienie, że Twoja aplikacja jest dla nich dobrym serwerem.
Prześledzimy Jetpackera, naszą przykładową aplikację podróżniczą, od pierwszej deklaracji narzędzia do rzeczywistego wywołania funkcji przez Gemini.
Możesz napisać funkcje AppFunctions samodzielnie lub skorzystać z pomocy agenta kodowania.
Udostępniliśmy umiejętność AppFunctions, która może pomóc Ci zacząć pisać funkcje. Możesz je połączyć z Android Studio za pomocą interfejsu wiersza poleceń Androida lub używać ich z wybranym agentem kodowania.
android skills add appfunctionsUmiejętność działa w 3 krokach.
Najpierw analizuje bazę kodu, aby zidentyfikować podstawowe ścieżki użytkownika, które mogą być przydatnymi narzędziami, takimi jak odtwarzanie multimediów, tworzenie notatki czy wysyłanie wiadomości.
Następnie generuje deklaracje AppFunction, które ujawniają te ścieżki wraz z wymaganą implementacją i obsługą platformy.
Na przykład Gradle i KSP, plik XML z metadanymi aplikacji i wstrzykiwanie zależności.
Na koniec ulepszy opisy KDoc, ponieważ to właśnie ich agent używa do decydowania, kiedy i jak wywołać funkcję.
Oto typ wyniku, którego szukamy.
To jest funkcja addExpense dla Jetpackera.
/**
* 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))
}Opis to jednozdaniowe zdanie, które zaczyna się od czasownika i zasobu: dodaje wydatek do podróży.
Pola przejazdu i kategorii mogą mieć wartość null, ponieważ użytkownicy nie zawsze je określają, ale funkcja rozwiąże je w treści.
Jeśli żądanie nie może zostać zrealizowane, zgłasza błąd AppFunctionException, dzięki czemu agent wie, co poszło nie tak.
OK, teraz przyjrzyjmy się testowaniu.
Twoje funkcje są teraz usługami, z których mogą korzystać agenty.
Musisz przetestować 2 elementy: działanie funkcji i działanie agenta.
W przypadku funkcji zaczniemy od testu jednostkowego, który prawdopodobnie został już przez Ciebie napisany, ale musisz pamiętać o kilku rzeczach, gdy to agent wywołuje funkcję.
Po pierwsze, wartości domyślne Kotlin nie mają tu zastosowania.
Pominięty parametr jest traktowany jako null, false lub 0.
Limit, który wcześniej domyślnie wynosił 10, będzie wynosił 0.
Wywołanie również rozpoczyna się w wątku głównym, a wywołanie funkcji z testu nie zawsze może to wyjaśnić, dlatego standardowe operacje wejścia-wyjścia należy uruchamiać w dyspozytorze działającym w tle.
Jeśli coś się nie powiedzie, zgłoś błąd AppFunctionException.
Wszystkie inne błędy będą docierać do agenta jako ogólny nieznany błąd z dołączonym nieprzetworzonym komunikatem o wyjątku.
Dane wejściowe będą pochodzić z modelu, więc zdecydowanie warto przetestować scenariusze skrajne, takie jak puste pola, synonimy dolarów, np. USD, i wartości spoza zakresu.
Po stronie agenta musisz wiedzieć, czy agent wybiera narzędzie we właściwym czasie i czy prawidłowo wykonuje funkcję.
Każdy agent zachowuje się nieco inaczej, dlatego ważne jest, aby przetestować funkcje z każdym agentem, który ma ich używać.
Do testowania możesz użyć naszego przykładowego agenta open source lub Gemini.
Jeśli chcesz zobaczyć, jak Gemini współdziała z Twoimi funkcjami AppFunctions, lub uruchomić Android MCP w aplikacji Gemini, możesz to zrobić za pomocą platformy Connected Apps Platform, którą uruchomiliśmy wcześniej i która jest obecnie dostępna w wersji testowej.
Więcej informacji znajdziesz w naszej wcześniejszej prezentacji na temat rozszerzania aplikacji na Gemini i wyszukiwarkę za pomocą platformy Connected Apps Platform.
Gdy dołączysz do programu wersji testowej, proces testowania za pomocą Gemini będzie składał się z 2 kroków.
Najpierw w Połączonej konsoli Google utworzymy połączoną aplikację, z której Gemini będzie mógł korzystać.
Po drugie, w Konsoli Play musimy przesłać pakiet aplikacji, aby uzyskać dostęp do funkcji aplikacji, i rozpocząć ścieżkę testu wewnętrznego, aby testerzy mogli faktycznie zweryfikować funkcje.
Zacznijmy od Połączonej konsoli Google.
Tutaj utworzysz połączoną aplikację, klikając przycisk „Utwórz aplikację” w prawym górnym rogu.
Podczas tworzenia połączonej aplikacji wybierz na tym ekranie Android MCP.
Jeśli tworzysz implementację MCP po stronie serwera, w tym miejscu możesz przesłać kod serwera.
Gdy klikniesz „Skonfiguruj”, pojawią się tu wszystkie aplikacje Google Play powiązane z Twoim kontem dewelopera, które możesz połączyć z połączoną aplikacją.
Jeśli nie masz jeszcze aplikacji w Google Play, musisz ją najpierw utworzyć w Konsoli Play.
W tym przypadku mamy już Jetpackera, więc go połączymy.
Następnie dodaj szczegóły, które użytkownicy zobaczą w interfejsie Gemini, gdy spróbują wejść w interakcję z Twoją aplikacją.
Obejmuje to takie elementy jak ikona aplikacji, wyświetlana nazwa, funkcje połączonej aplikacji i przykładowe prompty, których użytkownicy mogą używać do interakcji z nią.
Powinny być opisowe, aby jak najlepiej kierować użytkowników.
To już wszystko, jeśli chodzi o konfigurowanie połączonej aplikacji, ale możesz się zastanawiać, jak połączona aplikacja uzyskuje dostęp do funkcji.
Funkcje są zawarte w kodzie aplikacji, więc musimy teraz przejść do Konsoli Play, przesłać pakiet aplikacji i utworzyć ścieżkę testów, aby przetestować aplikację z udziałem rzeczywistych użytkowników.
Na tym ekranie prześlemy pakiet aplikacji, a następnie zobaczymy, że Google Play może wykryć AppFunctions i możesz je połączyć bezpośrednio z połączoną aplikacją.
Teraz widzimy połączoną aplikację Jetpack ze zintegrowanymi funkcjami.
Następnie musimy dodać testerów, aby ktoś mógł zweryfikować funkcje i upewnić się, że działają prawidłowo.
Kliknij tutaj wersje połączonej aplikacji.
Na tym ekranie możesz wybrać opcję „Zarządzaj testerami” i skonfigurować ograniczoną listę testerów, którzy będą mogli uzyskać dostęp do Twojej połączonej aplikacji w Gemini, gdy będzie ona dostępna.
Możesz dodać testerów do tej ścieżki testów w taki sam sposób jak w przypadku innych wersji.
Upewnij się, że lista testerów połączonej aplikacji jest taka sama jak w przypadku aplikacji w Google Play.
Teraz przetestujemy integrację.
Oto aplikacja Jetpacker na telefonie użytkownika, który ma już zapisaną podróż na ten szczyt.
Otworzę Gemini, aby połączyć aplikację.
Muszę upewnić się, że na moim urządzeniu jest zainstalowana ta sama wersja aplikacji, którą wcześniej przesłaliśmy do Google Play, i że używamy konta Gemini, które zostało dodane do listy testerów.
Wspomnę o Jetpackerze i poproszę o numer potwierdzenia rezerwacji pokoju hotelowego w Mountain View.
Gemini wywołuje funkcje AppFunctions Jetpackera na urządzeniu i odpowiada: „RED11223 w hotelu Redwood Grove”.
Następnie zapytam: „Ile budżetu zostało mi na reklamy?”.
Wywołanie funkcji zwróci wartość 1850 PLN.
Teraz chcę wprowadzić zmianę, więc poproszę Gemini o dodanie 45 USD za Ubera na lotnisko do mojej podróży na ADS.
Gemini wywołuje naszą funkcję addExpense, aby dodać to bezpośrednio do mojego planu podróży.
Gdy otwieram Jetpackera, wydatek jest już widoczny, a budżet odpowiednio zmniejszony.
Jeśli chcesz zacząć tworzyć połączone aplikacje dla Gemini już dziś, poproś o dostęp do platformy Connected Apps Platform, która jest obecnie w wersji testowej.
OK.
Podsumujmy.
Jesteśmy na początku ogromnej zmiany w dziedzinie urządzeń mobilnych i najwyższy czas, aby upewnić się, że Twoja aplikacja jest na nią gotowa.
Zacznij od określenia podstawowych ścieżek użytkownika o wysokiej wartości w aplikacji.
Udostępnij backend chmury za pomocą MCP po stronie serwera i używaj MCP na Androidzie do pracy z w pełni zaszyfrowanymi danymi, operacjami lokalnymi, czujnikami urządzenia i innymi elementami na urządzeniu.
Aby utworzyć prototyp, możesz pobrać Android Studio i interfejs wiersza poleceń Androida.
Możesz dodać zależność AndroidX AppFunctions w wersji beta do plików kompilacji i przetestować lokalnie pierwszą funkcję na urządzeniu.
Aby zarejestrować się i uzyskać wcześniejszy dostęp do Gemini w środowisku produkcyjnym, możesz zeskanować ten kod QR.
Oczywiście zapoznaj się też z tymi powiązanymi prelekcjami na temat pracy z Connected Apps Platform, a także o tym, jak możesz używać wewnętrznych funkcji aplikacji z ML Kit lub z własnym agentem, aby napędzać własnego agenta w aplikacji.
Dziękuję za poświęcony czas.
Nie możemy się doczekać, aż zobaczymy, jakie wspaniałe funkcje stworzysz na Androida.
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))
}