מנהל מוצר ב-Google
מערכת Android מתפתחת למערכת חכמה שבה סוכנים כמו Gemini מתאמים משימות בין אפליקציות. כדי לאפשר למשתמשים ליהנות מחוויות שימוש חדשות ולהשתתף ב-Agentic Workflows האלה, האפליקציה שלכם צריכה לחשוף את היכולות העיקריות שלה.
בסשן הזה נסביר איך לשלב את האפליקציה שלכם עם שכבת ה-AI של Android. נסקור את נתיבי השילוב העיקריים – כולל MCP של Android במכשיר, MCP בצד השרת ואוטומציה של משימות – ונספק מסגרת לקבלת החלטות כדי לבחור את הנתיב המתאים לתרחיש לדוגמה שלכם. נבדוק גם חוויות שימוש יזומות עם "הקשר אישי".
כאן תלמדו איך:
AppFunctionDeclaration, מספקים תיאורי KDoc ונמנעים מהסתמכות על ערכי ברירת המחדל של Kotlin.תמליל
טוב.
שלום לכולם,
אני דן גלפין מצוות Developer Relations.
ואני שרה שמסי, מנהלת מוצר שעובדת על סביבות הפיתוח החכמות של Android.
אנחנו שמחים מאוד שהצטרפתם לפגישה היום.
אנחנו עדים לשינוי פרדיגמה באופן שבו המשתמשים מקיימים אינטראקציה עם המכשירים שלהם.
בעידן האינטרנט, המשתמשים בדרך כלל התחילו את התהליך שלהם במעבר לדפדפן האינטרנט ואז בשימוש בפונקציית החיפוש.
בעידן הנייד, ראינו שינוי בהתנהגות המשתמשים, שהתחילו להשתמש באפליקציות נייטיב ייעודיות יותר כדי לבצע פעולות חד-פעמיות.
נראה דוגמה.
אני מקבל הודעה מחבר שמזמין אותי לארוחת ערב.
כדי לענות, אני צריך לבדוק ביומן אם אני פנוי, להוסיף את האירוע ליומן, להזמין נסיעה אם אני צריך דרך להגיע לשם, ואז לשלוח הודעה לחבר שלי שאני יכול להגיע.
בעבר, כדי לעשות את זה הייתי צריך לעבור בין אפליקציות שונות במכשיר כדי להשלים את הפעולות ולסיים את התהליך.
אבל עכשיו אנחנו בעידן הבינה המלאכותית.
בעידן ה-AI, המשתמשים מתחילים את התהליך בהבעת כוונות ויעדים ברמה גבוהה, במקום בפעולות ספציפיות שהם צריכים לבצע.
יש שינוי מהותי באופן שבו הם רואים את המכשיר שלהם – לא רק כמשהו שמאפשר להם לבצע פעולות, אלא כמשהו שבאופן יזום צופה את הצרכים שלהם ועוזר להם להשיג את המטרות שלהם.
ושמנו לב שהם מצפים שמסגרת הבינה הזו תפעל לא רק במכשירים הניידים שלהם, אלא במערכת אקולוגית עשירה של גורמי צורה, כולל שעונים, מכוניות, טלוויזיות, מחשבים ניידים, מכשירי XR ומשקפיים.
בעידן ה-AI, הנחיה אחת שמשלבת כמה אפליקציות יכולה לעזור לכם להזמין חברים לארוחת ערב, כמו בדוגמה שציינו קודם.
באמצעות הנחיה אחת בלבד, סוכן במכשיר שלי יכול לעזור לי לבצע את כל הפעולות שאני צריך: לבדוק את היומן, להוסיף את האירוע, להזמין את הנסיעה ולשלוח הודעה – והכול בבת אחת.
היום נשתף מידע נוסף על התוכניות של Android בתחום הזה, ונסביר איך אתם כמפתחים יכולים ליצור חוויות משתמש ברמה גבוהה.
דן.
תודה, שרה.
לכן, אנחנו ממשיכים להוסיף יכולות AI לליבה של Android, וכך האפליקציות שלכם יכולות להציע דרכים חדשות להתכונן לעידן ה-AI.
הראשון מבין שני עמודי התווך האלה של בינה מלאכותית למפתחים הוא בינה מלאכותית בתוך האפליקציה, והמטרה שלו היא כמובן להפוך את האפליקציות שלכם לחכמות יותר, להביא בינה מלאכותית לאפליקציה שלכם, ואתם יכולים להשתמש במודלים במכשיר כמו Gemini Nano 4 דרך ML Kit כדי לסכם שרשורי הודעות, לשכתב תשובות ולהבין תמונות, או להשתמש בשפה טבעית כדי להפעיל את הפונקציות של האפליקציה שלכם.
אתם יכולים אפילו להשתמש במודלים של Gemini בענן ישירות באפליקציה שלכם באמצעות Firebase AI Logic.
אבל היום נתמקד בעמוד התווך השני, ונוודא שהיכולות של האפליקציה שלכם זמינות במערכת החכמה.
אז מהי מערכת חכמה?
אז בנוסף לשיחה ולאירוח של החוויות המונחות ששרה דיברה עליהן קודם, זו מערכת שמאפשרת שילובים עמוקים כדי לשפר את החוויות בסביבה העסקית של האפליקציה.
לכן, מכשירים שמיועדים ל-Gemini Intelligence, למשל, משתמשים בנקודות השילוב האלה כדי לספק דברים כמו הצעות, סיכומי התראות, קלט קולי משופר ומילוי אוטומטי של טפסים.
עכשיו ההצעות הן חלק מתכונה שנקראת "הצעות שימושיות".
עד עכשיו דיברנו על עזרה שניתנת בתגובה להנחיה.
כולנו רגילים לזה: המשתמש אומר לסוכן לבצע משימה אחת או יותר בשמו.
אבל הצעות שימושיות הן בעצם חיזוי של הפעולה או הפעולות הבאות שהמשתמש עשוי לבצע.
אז אם נחזור לדוגמה של ארוחת הערב, הסוכן יכול להציע למשתמש באופן יזום הצעות בתגובה להודעה, שיכולות להוסיף את ההזמנה ליומן או להזמין נסיעה משותפת.
אבל איך זה בעצם מציג את ההנחיה למשתמש?
התכונה "עזרה יזומה" יכולה להציג פעולות במקומות כמו מסך הנעילה, ההתראות, המקלדות הווירטואליות ואפילו באפליקציות נבחרות.
אז איך אתם יכולים לגרום לאפליקציה שלכם להופיע בצ'יפים האלה כדי למשוך תנועה?
אתם כבר עושים דברים כמו פיתוח מול גרסת ה-SDK העדכנית ביותר ופיתוח עם הכלים הכי מודרניים שלנו, אבל הדבר הכי טוב שאתם יכולים לעשות עכשיו הוא לוודא שהאפליקציה שלכם מוכנה גם לסיוע בהנחיה, ששרה תדבר עליו בהמשך.
תודה, דן.
אוקיי, אז סיוע לפי בקשה כולל משתמשים שפונים ישירות לנציג ומבקשים ממנו להתחיל ולתזמר משימות, אפילו באפליקציות שונות.
כך אפשר להפוך תהליכי עבודה ידניים וגוזלי זמן לאוטומטיים.
במערכת החכמה שלנו ב-Android, אנחנו מתמקדים בעיקר בסוכני מערכת כמו Gemini שיכולים לגשת לאפליקציות במכשיר של המשתמש.
יש כמה דרכים שונות שבהן האפליקציה יכולה להופיע בממשק הזה ולקיים אינטראקציה עם הסוכן של המערכת.
יש את הפתרון שלנו לאוטומציה של משימות שמבוסס על הפלטפורמה, ויש שילובים ישירים של ממשקי API בסוכן המערכת.
אנחנו תמיד ממליצים להשתמש בשילובים ישירים של API כשהדבר אפשרי.
ההגדרות האלה נשלטות על ידי המפתח, כך שאתם יכולים להחליט ולקבוע איך האפליקציה והמותג שלכם יוצגו בפועל.
הן גם מהירות ואמינות יותר, ומציעות חוויה איכותית יותר למשתמשים.
יש שני נתיבי API שאפשר להשתמש בהם כדי לשלב את הסוכן.
Model Context Protocol, או בקיצור MCP, הוא השיטה המומלצת בתחום לקריאה להפעלת פונקציות בצד השרת.
אבל כשמדובר בקריאות API לכלים במכשיר, אנחנו מכריזים באופן רשמי על Android MCP – מסגרת מאוחדת לקריאה להפעלת פונקציות ב-Android.
Android MCP מופעל על ידי ספריית Jetpack AppFunctions, שמתאימה מאוד ל-MCP, גם מבחינה טכנית וגם מבחינה רעיונית, תוך תמיכה בהטמעה מוכרת ואידיומטית של Kotlin שהרבה מפתחים של Android מכירים.
כך קל ואינטואיטיבי יחסית להטמיע את ה-API, ואתם יכולים להשתמש בכל מה שלמדתם משילוב MCP בצד השרת שכבר הגדרתם.
הרבה פרימיטיבים של MCP ממופים אחד לאחד למבנה של Jetpack AppFunctions.
ריכזנו כאן כמה דוגמאות כדי להמחיש את זה.
נקודת קצה של כלי MCP ממפה בצורה טובה לפונקציית Kotlin עם הערה AppFunctionDeclaration.
המודלים של pydantic ממופים למחלקות נתונים של Kotlin עם הערות של AppFunctionSerializable.
אבל במקום פרוטוקול HTTP שניתן להעביר בסטרימינג או העברת קלט/פלט רגילה, Android MCP פועל על Android Binder IPC שעבר אופטימיזציה.
היום אנחנו שמחים לשתף שספריית Jetpack, AppFunctions, נמצאת עכשיו בגרסת בטא.
אוקיי, אז בטח אתם תוהים מתי כדאי להשתמש בנתיב API אחד, ומתי כדאי להשתמש ב-Android MCP, באופן ספציפי.
באופן כללי, Android MCP שימושי במיוחד כשרוצים להשתמש בהקשר מקומי או במכשיר כדי להשלים את התהליך שעובר המשתמש.
בואו נראה כמה דוגמאות שבהן Android MCP באמת מצטיין.
באופן ספציפי, כדאי להשתמש ב-Android MCP כשצריך לגשת לנתונים מוצפנים מקומיים.
זה חשוב במיוחד לאפליקציות של הודעות ותקשורת, שבהן היסטוריית ההודעות צריכה להישאר במכשיר בגלל ההצפנה מקצה לקצה.
Android MCP שימושי גם לפעולות שמתבצעות רק במכשיר ובמצב אופליין, אם הסוכנים תומכים בכך.
השותפה שלנו ב-Wear OS, Samsung Health, השיקה פונקציה שמאפשרת להתחיל ריצה ותרגילים, והכול מנוהל במכשיר עצמו.
משתמשים רוצים שהפעולות האלה יתבצעו במהירות, באמינות גבוהה, בזמן אחזור קצר, ולפעמים אפילו בסביבות עם קישוריות נמוכה. כדי להבטיח שזה יקרה, צריך לוודא שכל התהליך של קריאת ה-API לכלי, הניתוב והניתוב של השאילתות יכול להתבצע במכשיר.
Android MCP הוא גם נתיב ה-API המועדף אם רוצים לגשת לחיישני המכשיר או לקיים אינטראקציה ישירה עם חומרת המכשיר.
לדוגמה, אפליקציות רבות לרכב רוצות לאפשר למשתמשים לפתוח את דלתות הרכב באופן מיידי באמצעות קישוריות UWB, ולעיתים קרובות ללא שימוש בידיים.
אז Android MCP יכול לעזור בזה.
ולבסוף, זה הפתרון אם אין Cloud Endpoints זמינים לאפליקציות או ל-API שלכם.
הרבה מהאפליקציות של יצרני המכשירים שלנו הן אפליקציות מאינטראקציה ישירה (First-Party) בצד הלקוח בלבד, ולכן הן משתמשות ב-Android MCP.
לדוגמה, אפשר להשתמש ב-Gemini כדי לחפש ולאחזר תמונות מאפליקציות גלריה שהן בצד הלקוח בלבד, כמו Xiaomi Gallery.
כאן אני יכול לפתוח את Gemini ולבקש: "תמצא את התמונות של הרובוט של Android שצילמתי בשבוע שעבר ב-Xiaomi Gallery".
Xiaomi חשפה פונקציה של אפליקציה לחיפוש תמונות, ש-Gemini מפעיל, והתמונות הרלוונטיות מוחזרות ומוצגות ישירות בממשק המשתמש של אפליקציית Gemini, כך שהמשתמש לא צריך לצאת מהאפליקציה.
אנחנו מסתמכים על אותה ארכיטקטורה בדיוק לפעולות במכשיר ב-Android OS.
Android MCP מפעיל את התכונה "עזרה במכשיר" שהושקה לאחרונה, שהיא שילוב ישיר של Android OS באפליקציית Gemini.
היכולת הזו כבר הושקה במכשירי Pixel ו-Lenovo, ועכשיו היא מתרחבת למערכת האקולוגית הרחבה יותר של יצרני ציוד מקורי (OEM) של Android.
כדי להשתמש בעזרה במכשיר, המשתמשים יכולים לעבור אל Gemini ולשאול איך הם יכולים לשפר את ההגדרות שלהם, את הגדרות המכשיר ואת הגדרות המערכת כדי לשפר את חוויית השימוש במכשיר.
לדוגמה, במקום לחפש בתפריטים שונים, משתמש עיוור צבעים יכול לשאול את Gemini, "אני עיוור צבעים.
איך אפשר להתאים את הטלפון הזה לצרכים שלי בצורה טובה יותר?"
Gemini יפעיל פונקציות של אפליקציות מקומיות, ינתח את הבקשה בצורה חכמה ויציע דרכים שבהן המשתמש יכול לשנות את הגדרות המערכת והמכשיר כדי לשפר את החוויה.
אחרי זה, אמצעי הבקרה באפליקציה מופיעים ישירות בממשק המשתמש של אפליקציית Gemini, כדי שהמשתמשים יוכלו לשלוט בהם באופן מיידי.
לדוגמה, כדי למלא את הבקשה הזו לאופטימיזציה לעיוורי צבעים, Gemini הציג את המתג הזה לתיקון צבעים, כך שהמשתמשים יכולים לבצע את השינוי תוך כדי תנועה.
חוויות MCP ב-Android כבר זמינות במכשירים נבחרים, ואנחנו עובדים עם כמה שותפים כדי להביא עוד תרחישי שימוש לסביבה העסקית של Android.
הצהרות מפורשות על כלי MCP ל-Android הן הסטנדרט האידיאלי, אבל אנחנו יודעים שיהיו מקרים שבהם המשתמשים יבקשו מהסוכנים להשלים תרחישים מורכבים או פחות נפוצים. במקרים כאלה, התכונה "אוטומציה של משימות" בפלטפורמה שלנו נכנסת לתמונה.
במודל הזה, הסוכן שנטען מראש על ידי ה-OEM יכול להשתמש בממשקי ה-API של Android לשליטה במחשב כדי לדמות אירועי מגע בממשק של האפליקציה.
הסוכן מנתח את מסך האפליקציה ולוחץ על ממשק המשתמש כמו משתמש רגיל, כך שהמפתחים לא צריכים לשנות את הקוד.
כדי להבטיח את השליטה והבטיחות של המשתמשים, חשוב שהסוכנים יעצרו לפני פעולות שעלולות להיות רגישות, כמו תהליכי תשלום, ויבקשו אישור מהמשתמש כדי להמשיך.
בואו נראה דוגמה.
בדוגמה הזו, משתמש צילם תמונה של סיור קפה ושוקולד, והוא רוצה לבקש מ-Expedia או מ-Gemini לחפש ב-Expedia סיור דומה, אבל להתאים את מספר המשתתפים לשישה אנשים.
בלי צורך בשילוב API ישיר, Gemini מנווט בממשק המשתמש של Expedia ברקע באותו אופן שבו משתמש היה עושה זאת.
אחרי שהיא מוצאת אפשרויות רלוונטיות, היא מציגה אותן למשתמש ומעבירה לו את הטיפול בצורה חלקה כשהוא צריך לאשר סופית ולבצע את ההזמנה.
חשוב להבין ששלושת הנתיבים השונים האלה לא סותרים זה את זה, ולמעשה הם פועלים טוב יותר ביחד.
המשתמשים והמפתחים צריכים לצפות שהסוכנים יוכלו לתאם בין כלים בהטמעות שונות בשרת, וגם באפליקציית Android המקומית, כדי להציע למשתמשים את החוויה המקיפה וההוליסטית ביותר.
טוב.
תודה, שרה.
עכשיו נתעמק קצת יותר ב-MCP ל-Android.
כשאפשר, אני רוצה שתדעו שאני שולף את קטעי הקוד של MCP מ-Jetpacker, שהיא האפליקציה לדוגמה שלנו לנסיעות, שנבנתה בפועל בעזרת Antigravity.
ראיתם את זה בנאום המרכזי.
הוא עמוס בכל מיני תכונות חכמות ומגניבות, כולל שילובים במכשיר עם ממשקי ה-API של ML Kit Gen AI ו-ML Kit לזיהוי דיבור.
עכשיו ל-MCP ב-Android ול-MCP מבוסס-ענן יש מבנה דומה.
בשניהם יש לקוחות ושרתים.
בעולם של MCP, השרתים מספקים כלים, והלקוחות יכולים לגלות את הכלים האלה ולהפעיל אותם.
עכשיו, עם Android MCP, האפליקציה שלכם היא השרת והסוכן הוא הלקוח.
פלטפורמת Android משמשת כמאגר הכלים.
עכשיו האפליקציות מתחילות ברישום שירות כדי לספק את השרת, ואנחנו נתחיל בהוספת יחסי התלות של ספריית AppFunctions Jetpack בדוגמה שלנו כאן.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)ואז נשתמש בהערות KSP כדי ליצור AppFunctionService.
עכשיו, השירות הזה מאפשר לפונקציות של האפליקציה לפעול בהתאם למחזורי החיים של Android.
אנחנו יכולים להשתמש בהחדרת תלות עם Hilt כדי להחדיר מאגרי נתונים שבהם AppFunctions יכולות להשתמש.
@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
...סליחה.
נחזור לתרשים שלנו.
אחרי שמגדירים את השירות לשרת Android MCP, האפליקציה רושמת כלים בצורה של AppFunctions, יחד עם כל מודלי הנתונים המשויכים שבהם הם משתמשים.
עכשיו אפשר לרשום מודל נתונים באמצעות ההערה AppFunctionSerializable, וה-KDoc הזה כאן די מגניב.
/** 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 כדי לרשום את הפונקציה ככלי.
@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) }
}
}כשהערך של isDescribedByKDoc מוגדר כ-true, KDoc מספק לסוכן תיאור כדי להבין מה הפונקציה עושה, וזה גם עוזר לבני אדם להבין אותה.
/**
* 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)קודם כול, כדאי להתמקד בקבוצת המשנה המינימלית של הפרמטרים עם שמות ברורים ומובחנים.
הגדירו פרמטרים לא חיוניים כניתנים ל-null ופתרו את ברירות המחדל שלהם בגוף הפונקציה.
זו לא רק שיטה מומלצת.
זה פשוט לא יעבוד, כי המערכת תתעלם מערכי ברירת המחדל של Kotlin, כי ספריית AppFunctions מעבירה ערך null לפרמטרים שניתן להשמיט.
אני אפילו לא אראה דוגמה לזה, אבל אל תשתמשו בפרמטרים כוללים.
דברים כמו הקשר מלא או היסטוריית שיחות, הם פשוט לא טובים.
הם יכולים לגרום לנציגים להעביר כמויות גדולות של נתונים ולהוביל לזליגת מידע, וזה פשוט נוהג לא טוב.
כברירת מחדל, AppFunction פועלת בשרשור ה-UI, לכן חשוב לוודא שכל פעולה שנמשכת זמן רב מתרחשת ברקע. כדי לעשות את זה, צריך לבחור את ה-coroutine dispatcher המתאים.
@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 }
...כדאי להפעיל חריגות של AppFunction כשצריך, עם מידע מפורט כדי שהסוכן יוכל להבין איך לפתור שגיאה.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}אוקיי, אז עכשיו יש לנו AppFunction. בואו נחזור לדיאגרמת הארכיטקטורה.
אפליקציית הלקוח של הנציג נרשמת בפלטפורמה כדי להפעיל את AppFunctions.
לדוגמה, Gemini.
כדי לעשות את זה, הם צריכים הרשאה מיוחדת. לאחר מכן, סוכנים רשומים יכולים לחפש מטא-נתונים של AppFunction מפלטפורמת Android ולהחליט אם להפעיל פונקציית AppFunction רשומה אחת או יותר כדי למלא את הבקשה של המשתמש.
אם כבר מדברים על AppFunctions, איך הנציג מחליט בפועל?
הדבר האחרון שצריך לעשות הוא לתאר את היכולות של האפליקציה בקובץ מטא-נתונים של האפליקציה.
<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" />הסוכנים בוחרים כלים על ידי קריאת התיאור הזה בתוך AppFunctionAppMetadata, ואז קריאת התיאורים של פונקציות האפליקציה הספציפיות.
לכן, כדאי להתמקד בתחילת התיאור בהסבר קצר על התחום העיקרי של האפליקציה או הכלים שלכם.
אחרי זה, אתם יכולים להוסיף דפוסי פעולה שיעזרו לסוכן להבין את הזרימה הלוגית של הפעולות. זה שימושי כדי להגדיר דרישות מוקדמות או להנחות את הסוכן לנתיבים אופטימליים להשלמת המשימה.
אפשר גם להוסיף קטע על אילוצים שמגדיר מגבלות לוגיות של סוגי נתוני הקלט או מחזק דפוסי פעולה.
אלה היו הדברים הבסיסיים שצריך לדעת על Android MCP, אבל יש עוד כמה דברים שאפשר לעשות.
אפשרות אחת, שהיא די מגניבה, היא להפעיל ולהשבית את AppFunctions באופן דינמי.
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}זה חשוב, למשל, אם רוצים ש-AppFunction תהיה זמינה רק אם המשתמש מחובר לחשבון.
אפשר גם ליצור פונקציות AppFunctions שמוגבלות למסך או לפעילות ספציפיים באפליקציה.
@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
}לכן, במקום להצהיר על AppFunction, אתם מצהירים על AppFunctionSignature.
ואז אתם יכולים להטמיע handler ל-AppFunction בממשק המשתמש שלכם.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}והחלק הכי טוב הוא שמחזור החיים של ה-handlers קשור באופן אוטומטי למחזור החיים של ה-coroutine scope שבו אתם משתמשים.
עכשיו צריך לשמור במטמון את מתאם הבקשות של AppFunctions, סליחה, את מתאם הבקשות של handle AppFunctions, כי הספרייה משתמשת ברפלקציה כדי ליצור מופע שלו.
יש גם פונקציונליות ליצירת פונקציות AppFunctions ברמת ההיקף, שהרישום שלהן לא קשור ישירות למחזור החיים של Android.
עכשיו, דבר אחרון, יש לנו את הדבר הזה שנקרא רמת גישה.
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...אתם יכולים לקבל ולהשתמש ברמת גישה של self, system או trusted. הרמות האלה מציינות אילו פונקציות מיועדות לשימוש רק על ידי הסוכן שלכם באפליקציה, כמו סוכן שנבנה באמצעות ML Kit GenAI, אילו פונקציות זמינות רק למערכת ואילו פונקציות זמינות לסוכני מערכת מהימנים.
כדי לעזור לכם לפתח סוכן משלכם בתוך האפליקציה, הודענו כבר ש-ML Kit תומך באופן מובנה ב-AppFunctions כחלק ממסגרת הקריאות המאוחדת שלו לכלים.
אתם מגדירים אותם פעם אחת ומקבלים כיסוי גם לסוכנים ברמת המערכת וגם למודלים מקומיים בתוך האפליקציה, וכך אתם יכולים להימנע מהצהרות מפוצלות על כלים.
כדי לקבל מידע נוסף, אתם יכולים לצפות בהרצאה הזו, "איך להשתמש ב-ML Kit GenAI כדי להוסיף לאפליקציה יכולות אג'נטיות", או פשוט לבדוק מה חדש ב-Jetpack.
זה די מגניב.
אוקיי, נחזור לשרה.
טוב.
אז דן הראה לכם איך Android MCP עובד מאחורי הקלעים.
בואו נראה איך זה עובד בפועל.
חשוב לזכור שהאפליקציה היא שרת והנציגים הם הלקוח, ולכן המטרה היא לוודא שהאפליקציה היא שרת טוב בשבילם.
אנחנו נשתמש ב-Jetpacker, אפליקציית הנסיעות לדוגמה שלנו, מההצהרה הראשונה על הכלי ועד לקריאה בפועל לפונקציה דרך Gemini.
אתם יכולים לכתוב את ה-AppFunctions בעצמכם או להיעזר בסוכן תכנות כדי להתחיל.
השקנו את המיומנות AppFunctions שיכולה לעזור לכם להתחיל לכתוב את הפונקציות שלכם, שאותן תוכלו לחבר ל-Android Studio דרך Android CLI או להשתמש בהן עם סוכן הקידוד לבחירתכם.
android skills add appfunctionsהיכולת פועלת בשלושה שלבים.
קודם כל, הוא מנתח את בסיס הקוד כדי לזהות את התהליכים העיקריים שהמשתמשים עוברים, שיכולים לשמש ככלים שימושיים, כמו הפעלת מדיה, יצירת הערה או שליחת הודעה.
אחר כך, הוא יוצר הצהרות של AppFunction שחושפות את התהליכים האלה, כולל ההטמעה והתמיכה בפלטפורמה שהם צריכים.
לדוגמה, Gradle ו-KSP, קובץ XML של מטא-נתונים של אפליקציה והזרקת תלות.
בסיום, הוא ישפר את תיאורי ה-KDoc, כי זה בדיוק מה שהסוכן משתמש בו כדי להחליט מתי להפעיל את הפונקציה ואיך.
זה סוג התוצאה שאנחנו מחפשים.
זו פונקציה של addExpense ב-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))
}התיאור הוא משפט בן שורה אחת שמתחיל בפועל ובמשאב: מוסיף הוצאה לנסיעה.
השדות של הנסיעה והקטגוריה יכולים להיות ריקים כי המשתמשים לא תמיד מציינים אותם, אבל הפונקציה תפתור אותם בגוף ההודעה.
אם בקשה נכשלת, היא מחזירה AppFunctionException כדי שהנציג ידע מה השתבש.
עכשיו נראה איך בודקים את זה.
הפונקציות שלכם הן עכשיו שירותים שהסוכנים יכולים להשתמש בהם.
לכן, יש שני חלקים שצריך לבדוק: ההתנהגות של הפונקציה וההתנהגות של הנציג.
לגבי הפונקציה, נתחיל עם בדיקת היחידה שכנראה כבר כתבתם, אבל יש כמה דברים שצריך לזכור כשהנציג הוא זה שמבצע בפועל את הקריאה להפעלת הפונקציה.
קודם כול, ערכי ברירת המחדל של Kotlin לא חלים כאן.
פרמטר שהושמט מגיע כ-null, false או 0.
לכן, מגבלה שבעבר הייתה 10 כברירת מחדל, תהיה כאן 0.
הקריאה גם מתחילה ב-thread הראשי, וקריאה לפונקציה מבדיקה לא תמיד תבהיר את זה, לכן כדאי להריץ את הקלט/פלט הרגיל ב-dispatcher ברקע.
אם משהו נכשל, זורקים AppFunctionException.
כל דבר אחר יגיע לסוכן כשגיאה גנרית לא ידועה, עם הודעת החריגה הגולמית מצורפת.
הקלט שלכם יגיע ממודל, אז כדאי לבדוק תרחישים של מקרי קצה כמו שדות ריקים, מילים נרדפות לדולרים כמו USD וערכים מחוץ לטווח.
בצד של הסוכן, כדאי לבדוק אם הסוכן בוחר את הכלי בזמן הנכון ואם הוא מפעיל את הפונקציה בצורה תקינה.
כל נציג מתנהג קצת אחרת, לכן חשוב לבדוק את הפונקציות עם כל נציג שאתם מצפים שישתמש בהן.
אתם יכולים להשתמש בנציג לדוגמה בקוד פתוח או ב-Gemini כדי לבדוק את השילוב הזה.
אם אתם רוצים לראות איך Gemini יוצר אינטראקציה עם AppFunctions או להפעיל Android MCP באפליקציית Gemini, אתם יכולים לעשות את זה באמצעות פלטפורמת האפליקציות המקושרות, שהשקנו מוקדם יותר היום והיא נמצאת כרגע בגרסת טרום-השקה (Preview).
כדי לקבל מידע נוסף, אתם יכולים לצפות בשיחה הקודמת שלנו על הרחבת האפליקציה ל-Gemini ולחיפוש Google באמצעות Connected Apps Platform.
אחרי שמצטרפים לתוכנית, התהליך לבדיקה עם Gemini כולל שני שלבים.
קודם, ב-Google Connected Console, ניצור אפליקציה מקושרת ש-Gemini יוכל להשתמש בה.
בשלב השני, צריך להעלות את ה-App Bundle ל-Play Console כדי שנוכל לגשת ל-AppFunctions. אחר כך אפשר להתחיל במסלול לבדיקה פנימית, כדי שהבודקים יוכלו לאמת את הפונקציות.
אז נתחיל ב-Google Connected Console.
כאן, יוצרים אפליקציה מקושרת באמצעות הלחצן "יצירת אפליקציה" בפינה השמאלית העליונה.
כשיוצרים את האפליקציה המקושרת, בוחרים באפשרות Android MCP במסך הזה.
אם אתם יוצרים הטמעה של MCP בצד השרת, כאן אתם צריכים להעלות את קוד השרת.
אחרי שתלחצו על "הגדרה", כל האפליקציות ב-Google Play שמשויכות לחשבון הפיתוח שלכם יופיעו כאן ויהיו מוכנות לקישור לאפליקציה המקושרת.
אם עדיין אין לכם אפליקציה ב-Google Play, תצטרכו קודם ליצור אחת ב-Play Console.
כאן כבר יש לנו את Jetpacker, אז נמשיך ונקשר אותו.
בשלב הבא, מוסיפים את הפרטים שהמשתמשים יראו בממשק של Gemini כשהם ינסו להשתמש באפליקציה.
המידע הזה כולל דברים כמו סמל האפליקציה, שם לתצוגה, היכולות של האפליקציה המקושרת ופרומפטים לדוגמה שהמשתמשים יכולים להשתמש בהם כדי לבצע אינטראקציה עם האפליקציה.
השמות צריכים להיות תיאוריים כדי לעזור למשתמשים בצורה הטובה ביותר.
זהו, סיימנו להגדיר את האפליקציה המקושרת, אבל אולי אתם תוהים איך האפליקציה המקושרת מקבלת גישה לפונקציות.
הפונקציות נמצאות בתוך קוד האפליקציה, אז עכשיו צריך לעבור ל-Play Console, להעלות את ה-App Bundle וליצור מסלול בדיקה כדי שנוכל לבדוק את האפליקציה עם משתמשים אמיתיים.
אז במסך הזה, נעלה את קובץ ה-App Bundle, ואז נראה כאן ש-Play יכול לזהות את AppFunctions, ואפשר לקשר אותן ישירות לאפליקציה המקושרת.
עכשיו אפשר לראות את האפליקציה המקושרת ל-Jetpack, כולל הפונקציות ששולבו.
בשלב הבא, נצטרך להוסיף בודקים, כדי שיהיה מישהו שמוכן לאמת את הפונקציות ולוודא שהן פועלות כמו שצריך.
כדי לעשות את זה, לוחצים על גרסאות האפליקציה המקושרות כאן.
במסך הזה, אתם יכולים לבחור באפשרות "ניהול הבודקים" ולהגדיר רשימה מוגבלת של בודקים שיוכלו לגשת לאפליקציה המקושרת ב-Gemini כשהיא תהיה זמינה.
אתם יכולים להוסיף בודקים למסלול הבדיקה הזה באותו אופן שבו אתם מוסיפים בודקים לכל גרסה אחרת.
חשוב לוודא שרשימת הבודקים של האפליקציה המקושרת זהה לרשימת הבודקים של האפליקציה ב-Google Play.
אוקיי, עכשיו נבדוק את השילוב.
זו האפליקציה Jetpacker בטלפון של משתמש, שכבר יש בה נסיעה מתועדת לפסגה הזו.
אפתח את Gemini כדי לקשר את האפליקציה.
אני צריך לוודא שבמכשיר שלי מותקנת אותה גרסה של האפליקציה שהעלינו ל-Play קודם, ושאנחנו משתמשים בחשבון ל-Gemini שנוסף לרשימת הבודקים.
אז כאן, אני אציין את Jetpacker ואבקש את מספר האישור של החדר שלי במלון במאונטיין ויו.
Gemini מפעיל את AppFunctions של Jetpacker במכשיר ועונה: "RED11223 במלון Redwood Grove".
אחר כך אשאל: "כמה תקציב נשאר לי ל-Google Ads?"
והבקשה להפעלת הפונקציה תחזיר את הערך 1,850$.
עכשיו אני רוצה לעשות שינוי, אז אבקש מ-Gemini להוסיף 45 $לנסיעה ב-Uber לשדה התעופה בנסיעה שלי ל-ADS.
Gemini מפעיל את הפונקציה addExpense ממקודם כדי להוסיף את זה ישירות למסלול הנסיעה שלי.
וכשאני פותחת את Jetpacker, ההוצאה כבר מופיעה שם, והתקציב יורד בהתאם.
אם אתם רוצים להתחיל לפתח אפליקציות מקושרות ל-Gemini כבר היום, אתם יכולים להגיש בקשה לגישה ל-Connected Apps Platform, שנמצאת כרגע בגרסת טרום-השקה (Preview).
טוב.
אז בואו נסכם.
אנחנו נמצאים בתחילתו של שינוי עצום במחשוב לניידים, וזה הזמן לוודא שהאפליקציה שלכם מוכנה.
לכן, קודם כל צריך לזהות את התהליכים העיקריים שהמשתמשים עוברים באפליקציה, שיש להם ערך גבוה.
אתם יכולים לחשוף את הקצה העורפי של הענן באמצעות MCP בצד השרת, ולהשתמש ב-MCP של Android כדי לעבוד עם נתונים מוצפנים מקצה לקצה, פעולות מקומיות, חיישני מכשירים ודברים אחרים במכשיר.
כדי ליצור אב טיפוס, אתם יכולים להוריד את Android Studio ואת Android CLI.
אתם יכולים להוסיף את התלות בגרסת הבטא של AndroidX AppFunctions לקובצי ה-build, ולבדוק את הפונקציה הראשונה במכשיר באופן מקומי.
כדי להירשם לגישה מוקדמת למעבר לסביבת ייצור עם Gemini, אתם יכולים לסרוק את קוד ה-QR הזה.
כמובן, כדאי לצפות בהרצאות הקשורות האלה על עבודה עם Connected Apps Platform, וגם על איך אפשר להשתמש ב-AppFunctions פנימיות עם ML Kit או עם סוכן משלכם כדי להפעיל סוכן משלכם בתוך האפליקציה.
ולסיום, תודה רבה על הזמן שהקדשת.
אנחנו כבר ממש סקרנים לראות את כל החוויות הנהדרות שאתם יוצרים ל-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))
}