مدیر محصول در Google
Android درحال تبدیل شدن به یک سیستم هوشمند است که در آن، عاملهایی مثل Gemini تکالیف را در برنامهها سازماندهی میکنند. برای باز کردن قفل تجربههای کاربری جدید و مشارکت در این گردشهای کاری عاملی، برنامهتان باید قابلیتهای اصلی خود را آشکار کند.
این جلسه راهنمایی عملی برای ادغام کردن برنامهتان با لایه هوش Android است. مسیرهای ادغام کلیدی را بررسی خواهیم کرد - ازجمله MCP Android دروندستگاهی، MCP سمت سرور، و «خودکارسازی تکلیف» - و چارچوب تصمیمگیری برای انتخاب مسیر مناسب برای مورد استفاده شما ارائه خواهیم داد. همچنین تجربههای پیشکنشی را با «زمینه شخصی» کاوش خواهیم کرد.
با روش انجام این کارها آشنا خواهید شد:
AppFunctionDeclaration اعلام کنید، شرحهای KDoc ارائه دهید، و از تکیه بر مقادیر پیشفرض Kotlin اجتناب کنید.آوانویسی
بسیار خوب.
سلام به همه.
من دن گالپین از بخش روابط توسعهدهندگان هستم.
و من سارا شمسی هستم، مدیر محصولی که روی چارچوبهای هوش توسعهدهنده Android کار میکند.
بسیار خوشحالیم که به جلسه امروز خوشامد بگوییم.
ما شاهد تغییر الگو در نحوه تعامل کاربران با دستگاههایشان هستیم.
در عصر وب، کاربران معمولاً سفر خود را با رفتن به مرورگر وب و سپس استفاده از عملکرد جستجو آغاز میکردند.
و سپس در عصر تلفن همراه، شاهد تغییر رفتار کاربر به استفاده از برنامههای تخصصیتر و بومی برای تکمیل اقدامات یکباره بودیم.
در اینجا مثالی آورده شده است.
پیامی از دوستم دریافت میکنم که من را به شام دعوت میکند.
برای پاسخ دادن به این پیام، باید تقویمم را بررسی کنم تا ببینم وقتم آزاد است یا نه، رویداد را به تقویمم اضافه کنم، اگر لازم باشد برای رفتن به آنجا خودرو رزرو کنم، و سپس به دوستم پیام دهم که میتوانم در رویداد شرکت کنم.
قبلاً این کار به چیزی به نام «پرش بین برنامهها» نیاز داشت، که در آن اساساً باید بین برنامههای مختلف در دستگاه خود جابهجا میشدم تا واقعاً اقدامات را تکمیل کنم و سفرم را به پایان برسانم.
اما اکنون در «عصر هوش» هستیم.
در «عصر هوش»، کاربران سفرشان را با بیان اهداف و مقاصد سطح بالا شروع میکنند، نه با کنشهای خاصی که میخواهند انجام شود.
تغییری اساسی در دیدگاه آنها نسبت به دستگاهشان ایجاد شده است؛ آنها دستگاه خود را بهعنوان چیزی میبینند که بهطور فعال نیازهایشان را پیشبینی میکند و به آنها کمک میکند تا به اهدافشان برسند، نه فقط چیزی که امکان انجام کارها را فراهم میکند.
و متوجه شدیم که آنها انتظار دارند این چارچوب هوش مصنوعی نهتنها در دستگاههای همراهشان، بلکه در سراسر بومسازگان غنی از عوامل شکل، ازجمله ساعت، خودرو، تلویزیون، رایانه کیفی، و واقعیت گسترده و عینک کار کند.
در «عصر هوش مصنوعی»، یک پیامواره بینبرنامهای میتواند مثال دعوت به شام را که قبلاً ذکر شد تسهیل کند.
فقط با یک پیامواره، نمایندهای در دستگاه من میتواند به فعال کردن همه کنشهایی که نیاز دارم کمک کند، از بررسی تقویمم، افزودن رویداد، رزرو سفر، و ارسال پیام، همه در یک حرکت.
امروز قصد داریم درباره طرحهای Android در این فضا و سپس نحوه ساختن تجربههای درجه یک توسط شما بهعنوان توسعهدهنده بیشتر صحبت کنیم.
Dan.
متشکرم، سارا.
بنابراین، Android همچنان به افزودن هوش به هسته خود ادامه میدهد و این امر روشهای جدیدی را برای برنامههای شما ارائه میدهد تا برای این عصر هوش آماده شوند.
و اولین ستون از این دو ستون هوش توسعهدهنده، هوش درونبرنامه است که هدف آن هوشمندتر کردن برنامههای شما و آوردن هوش به برنامهتان است. میتوانید از مدلهای دروندستگاهی مانند Gemini Nano 4 ازطریق ML Kit برای خلاصه کردن رشتههای پیام، بازنویسی پاسخها، و درک تصاویر استفاده کنید، یا از زبان طبیعی برای فراخوانی توابع برنامهتان استفاده کنید.
حتی میتوانید از مدلهای Gemini در فضای ابری مستقیماً در برنامه خود ازطریق Firebase AI Logic استفاده کنید.
اما تمرکز امروز ما روی ستون دوم است، یعنی اطمینان از اینکه قابلیتهای برنامه شما در سیستم هوش مصنوعی دردسترس باشد.
بنابراین، سیستم هوش چیست؟
بنابراین، علاوهبر، میدانید، صحبت کردن و میزبانی کردن درباره آن تجربههای درخواستشده که سارا قبلاً درباره آنها صحبت کرد، این سیستم امکان ادغامهای عمیق را برای بهبود تجربهها در سراسر بومسازگان برنامه فراهم میکند.
بنابراین، دستگاههایی که برای هوش Gemini طراحی شدهاند، برای مثال، از این نقاط ادغام برای ارائه مواردی مانند پیشنهادها، اعلانهای خلاصه، ورودی صوتی بهبودیافته، و تکمیل خودکار فرمها استفاده میکنند.
اکنون پیشنهادها بخشی از ویژگیای بهنام «دستیاری پیشکنشی» هستند.
و تا اینجا، درباره کمک با پیامواره صحبت کردهایم.
همه ما به این موضوع عادت داریم که کاربر به نماینده میگوید یک یا چند کار را ازطرف او انجام دهد.
اما دستیاری پیشکنشی درواقع به پیشبینی کنش یا کنشهای بعدی که کاربر ممکن است انجام دهد مربوط میشود.
بنابراین، اگر به مثال شام برگردیم، نماینده ممکن است در پاسخ به پیام، بهطور فعالانه پیشنهادهایی را به کاربر ارائه دهد که میتواند دعوت را به تقویم او اضافه کند یا یک سفر اشتراکی رزرو کند.
اما درواقع چگونه از کاربر درخواست میکند؟
دستیاری پیشکنشی میتواند کنشها را در جاهایی مثل صفحه قفل، اعلانها، صفحهکلیدهای مجازی، و حتی در برنامههای منتخب نشان دهد.
بنابراین، چگونه میتوانید برنامه خود را در این تراشههای پیشکنشی نمایش دهید تا ترافیک را به سمت خود هدایت کنید؟
خب، شما درحالحاضر کارهایی مثل ساختن براساس جدیدترین نسخه کیت توسعه نرمافزار و ساختن با مدرنترین ابزارهای ما را انجام میدهید، اما بهترین کاری که اکنون میتوانید انجام دهید این است که مطمئن شوید برنامهتان برای کمک با پیامواره نیز آماده است، که سارا در ادامه درباره آن صحبت خواهد کرد.
ممنون، دن.
بسیار خوب، پس دستیار پیاموارهای شامل کاربرانی میشود که مستقیماً به یک عامل مراجعه میکنند و از آن میخواهند که کارهایی را، حتی در برنامههای مختلف، شروع و سازماندهی کند.
این کار میتواند گردشهای کاری را که قبلاً دستی و وقتگیر بودند خودکارسازی کند.
برای سیستم هوشمندمان در Android، تمرکز اصلی ما روی عاملهای سیستم مانند Gemini است که میتوانند به برنامهها در سراسر دستگاه کاربر دسترسی داشته باشند.
چند روش مختلف وجود دارد که برنامه شما میتواند در این تجربه نشان داده شود و با عامل سیستم تعامل داشته باشد.
راهکار «خودکارسازی تکلیف» مبتنی بر پلاتفرم ما وجود دارد و سپس ادغامهای مستقیم میانای برنامهسازی کاربردی در عامل سیستم وجود دارد.
توصیه میکنیم همیشه درصورت امکان با ادغامهای مستقیم API بسازید.
اینها تحت کنترل توسعهدهنده هستند، بنابراین میتوانید تصمیم بگیرید و کنترل کنید که برنامهتان و نمانامتان دقیقاً چگونه نشان داده شوند.
این برنامهها همچنین سریعتر و قابلاعتمادتر هستند و تجربه ممتازتری را به کاربران شما ارائه میدهند.
دو مسیر API وجود دارد که میتوانید هنگام ادغام با نماینده در نظر بگیرید.
«پروتکل بافتار مدل» یا بهاختصار MCP، بهترین روش صنعتی برای فراخوانی تابع سمت سرور است.
اما برای فراخوانی ابزار دروندستگاهی، بهطور رسمی Android MCP را معرفی میکنیم که چارچوب فراخوانی تابع یکپارچه در Android است.
Android MCP از کتابخانه Jetpack AppFunctions ما پشتیبانی میکند که هم ازنظر فنی و هم ازنظر مفهومی با MCP همراستا است و درعینحال از پیادهسازی آشنا و اصطلاحی Kotlin پشتیبانی میکند که بسیاری از توسعهدهندگان Android با آن آشنا هستند.
این امر پیادهسازی میانای برنامهسازی کاربردی را نسبتاً شهودی و آسان میکند و میتوانید از هرگونه دانشی که ممکن است از یکپارچهسازی MCP سمت سرور ازقبل راهاندازیشده داشته باشید استفاده کنید.
بسیاری از عناصر پایه MCP بهصورت یکبهیک به یک ساختار Jetpack AppFunctions نگاشت میشوند.
بیایید چند مثال را برای توضیح این موضوع بررسی کنیم.
نقطه پایانی ابزار MCP بهخوبی با تابع Kotlin Kotlin که با AppFunctionDeclaration حاشیهنویسی شده است مطابقت دارد.
مدلهای pydantic شما به کلاسهای داده Kotlin با حاشیهنویسی AppFunctionSerializable نگاشت میشوند.
اما بهجای HTTP قابلجاریسازی و/یا انتقال ورودی/خروجی استاندارد، MCP Android روی IPC بهینهشده Android Binder اجرا میشود.
امروز بسیار هیجانزدهایم که اعلام کنیم کتابخانه Jetpack، AppFunctions، اکنون در مرحله بتا است.
بسیار خوب، مطمئنم احتمالاً برایتان سؤال است که چه زمانی منطقی است از یک مسیر API استفاده کنید و چه زمانی منطقی است بهطور خاص از Android MCP استفاده کنید.
بهعنوان یک اصل کلی، Android MCP بهویژه زمانی مفید است که بخواهید از بافت محلی یا دروندستگاهی برای تکمیل مسیر کاربر استفاده کنید.
بیایید به چند نمونه بپردازیم که در آنها Android MCP واقعاً میدرخشد.
بهطور مشخص، وقتی نیاز به دسترسی به دادههای محلی رمزگذاریشده دارید، باید Android MCP را درنظر بگیرید.
این موضوع بهویژه برای برنامههای پیامرسانی و ارتباطی مهم است، زیرا سابقه پیامها بهدلیل رمزگذاری سرتاسر باید در دستگاه باقی بماند.
Android MCP برای کنشهای کاملاً آفلاین و دروندستگاهی نیز مفید است، درصورتیکه عاملها از این کنشها پشتیبانی کنند.
شریک Wear OS ما، Samsung Health، عملکردی را برای شروع دویدن و شروع تمرینات راهاندازی کرد که کاملاً در دستگاه سازماندهی میشود.
کاربران اغلب میخواهند این نوع کنشها بسیار سریع، با قابلیت اطمینان بالا، و تأخیر کم انجام شود و گاهی اوقات، حتی در محیطهای با اتصال ضعیف، اطمینان از اینکه کل فراخوانی و مسیریابی ابزار و مسیریابی پُرسمان میتواند در دستگاه انجام شود، این امر را تضمین میکند.
اگر میخواهید به حسگرهای دستگاه دسترسی داشته باشید یا مستقیماً با سختافزار دستگاه تعامل داشته باشید، Android MCP مسیر API ترجیحی است.
برای مثال، بسیاری از برنامههای خودرویی میخواهند به کاربران اجازه دهند قفل درهای خودرو را فوراً ازطریق اتصال فراپهنباند و اغلب بهصورت دستآزاد باز کنند.
بنابراین، MCP در Android میتواند در این زمینه کمک کند.
و درنهایت، اگر نقطه پایانی ابری دردسترسی برای برنامهها یا API شما وجود نداشته باشد، این راهحل شما است.
بسیاری از برنامههای طرف اول سازنده دستگاه ما فقط سمت کارخواه هستند و به همین دلیل از MCP Android استفاده میکنند.
برای مثال، میتوان از Gemini برای جستجو و بازیابی عکسها از برنامههای گالری که فقط سمت کارخواه هستند، مثل «گالری Xiaomi»، استفاده کرد.
اینجا میتوانم Gemini را باز کنم و بپرسم: «عکسهای روبات Android را که هفته گذشته در گالری Xiaomi گرفتم پیدا کن.»
Xiaomi عملکردی را در برنامه برای جستجوی عکسها ارائه کرده است که Gemini آن را فراخوانی میکند و عکسهای مربوطه مستقیماً در میانای کاربر برنامه Gemini برگردانده و نمایش داده میشوند، بنابراین کاربر هرگز مجبور نیست برنامه را ترک کند.
و ما برای کنشهای دروندستگاهی در سیستمعامل Android به همان معماری دقیق متکی هستیم.
Android MCP ویژگی «راهنمای دستگاه» را که اخیراً راهاندازی شده است تقویت میکند، که یکپارچهسازی مستقیم سیستمعامل Android در برنامه Gemini است.
این قابلیت قبلاً در دستگاههای Pixel و Lenovo راهاندازی شده است و درحالحاضر درحال گسترش در اکوسیستم گستردهتر سازنده اصلی محصول Android است.
کاربران میتوانند با رفتن به Gemini و پرسیدن اینکه چگونه میتوانند تنظیمات، تنظیمات دستگاه، و تنظیمات سیستم خود را برای بهبود تجربه دستگاهشان بهینهسازی کنند، از «راهنمای دستگاه» استفاده کنند.
برای مثال، بهجای جستجو در منوهای مختلف، کاربری که کوررنگی دارد میتواند از Gemini بپرسد: «من کوررنگی دارم.
با چه تغییراتی میتوانم این تلفن را برای خودم کاربردیتر کنم؟»
Gemini توابع برنامه محلی را فراخوانی و اجرا میکند و بهطور هوشمندانه درخواست را استدلال میکند و روشهای واقعی را پیشنهاد میدهد که کاربر میتواند تنظیمات سیستم خود را تغییر دهد و روشهای خاصی را پیشنهاد میدهد که میتواند تنظیمات دستگاه خود را برای بهبود تجربه تغییر دهد.
سپس، کنترلهای درونبرنامه مستقیماً در میانای کاربری برنامه Gemini ظاهر میشوند تا کاربران بتوانند فوراً کنترل را بهدست بگیرند.
برای مثال، در برآورده کردن این درخواست بهینهسازی برای کوررنگی، Gemini درواقع این کلید روشن/خاموش تصحیح رنگ را نشان داده است تا کاربران بتوانند این تغییر را در لحظه انجام دهند.
تجربههای MCP در Android درحالحاضر در دستگاههای منتخب دردسترس است و ما با چندین شریک همکاری میکنیم تا موارد استفاده بیشتری را به بومسازگان Android اضافه کنیم.
درحالیکه اعلام صریح ابزار MCP Android استاندارد طلایی ما است، میدانیم مواردی وجود خواهد داشت که کاربران از عاملها میخواهند موارد استفاده پیچیده یا کمتر رایج را تکمیل کنند، و اینجاست که ویژگی «خودکارسازی تکلیف» مبتنی بر پلاتفرم ما وارد عمل میشود.
تحت این مدل، عامل ازپیش بارشده سازنده اصلی محصول میتواند از میاناهای برنامهسازی کاربردی کنترل رایانه Android برای شبیهسازی رویدادهای لمسی در سطح برنامه استفاده کند.
این کار نیازی به تغییر کد ازسوی توسعهدهندگان ندارد و مستلزم آن است که نماینده با تجزیهوتحلیل صفحهنمایش برنامه، مانند یک کاربر روی واسط کاربر ضربه بزند.
برای اطمینان از کنترل و ایمنی کاربر، بسیار مهم است که عاملها در کنشهای بالقوه حساس مانند جریانهای پرداخت مکث کنند و برای ادامه کار، تأیید کاربر را درخواست کنند.
بیایید نگاهی به یک مثال در عمل بیندازیم.
در اینجا، کاربری از تور قهوه و شکلات عکس گرفته است و میخواهد از Expedia بپرسد، یا میخواهد از Gemini بخواهد Expedia را بررسی کند و ببیند آیا میتواند تور مشابهی پیدا کند، اما تعداد افراد را به شش شرکتکننده تغییر دهد.
بدون نیاز به ادغام مستقیم میانای برنامهسازی کاربردی، Gemini در پسزمینه همانند یک کاربر در میانای کاربر Expedia پیمایش میکند.
سپس گزینههای مربوطی را که پیدا کرده است نشان میدهد و وقتی زمان تأیید نهایی و رزرو کردن فرا میرسد، بهطور یکپارچه کار را به کاربر واگذار میکند.
مهم است بدانید که این سه مسیر مختلف مانع یکدیگر نیستند و درواقع در کنار هم بهترند.
کاربران و توسعهدهندگان باید انتظار داشته باشند که عاملها بتوانند ابزارها را در سراسر پیادهسازیهای سرور و همچنین برنامه Android محلی هماهنگ کنند تا جامعترین و کاملترین تجربه را به کاربران ارائه دهند.
بسیار خوب.
متشکرم، سارا.
میخواهیم کمی عمیقتر به Android MCP بپردازیم.
And when possible, I want you to know I'm pulling the MCP code snippets from Jetpacker, which is our travel sample app, which is actually built with the help of Antigravity.
این را در سخنرانی اصلی دیدهاید.
این برنامه با انواع ویژگیهای هوشمند جالب، ازجمله ادغامهای دروندستگاهی با ML Kit Gen AI و ML Kit speech recognition APIs، بارگیری شده است.
اکنون MCP Android و MCP مبتنی بر ابر ساختار مشابهی دارند.
هر دو دارای کارخواه و سرور هستند.
اکنون در دنیای MCP، سرورها ابزارها را ارائه میدهند و کلاینتها میتوانند این ابزارها را کشف و فراخوانی کنند.
اکنون با MCP Android، برنامه شما سرور است و عامل کارخواه است.
«پلاتفرم 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)قبلاز هر چیز، باید روی حداقل زیرمجموعه پارامترها با نامهای واضح و متمایز تمرکز کنید.
پارامترهای غیرضروری را تهیپذیر کنید و مقادیر پیشفرض آنها را در بدنه تابع حل کنید.
این فقط یک روال مطلوب نیست.
واقعاً کار نخواهد کرد، زیرا مقادیر پیشفرض Kotlin نادیده گرفته خواهند شد چون کتابخانه AppFunctions برای پارامترهای قابلحذف حذفشده مقدار null ارسال میکند.
حتی قصد ندارم مثالی از این مورد اول نشان دهم، اما از پارامترهای catch-all استفاده نکنید.
چیزهایی مثل بافت کامل یا سابقه مکالمه، اینها واقعاً بد هستند.
این کار میتواند باعث شود که عاملها حجم زیادی از دادهها را منتقل کنند و منجر به استخراج غیرمجاز اطلاعات شود، و این کار اصلاً درست نیست.
ازآنجاییکه AppFunction بهطور پیشفرض در رشته میانای کاربر اجرا میشود، باید مطمئن شوید که هر عملیات طولانیمدت با انتخاب توزیعکننده مناسب کوروتین در پسزمینه انجام میشود.
@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 ثبتشده را برای تکمیل درخواست کاربر اجرا کنند یا نه.
اکنون که درباره یک یا چند AppFunction صحبت میکنیم، نماینده واقعاً چگونه تصمیم میگیرد؟
خب، آخرین کاری که باید انجام دهید این است که قابلیتهای برنامهتان را در فایل فراداده برنامه شرح دهید.
<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 شما و سپس خواندن شرحها با AppFunctions تکی انتخاب میکنند.
بنابراین، باید ابتدای توضیحات خود را با توضیح مختصری از دامنه اصلی برنامه یا ابزارهای خود متمرکز کنید.
و میتوانید این را با الگوهای عملیاتی دنبال کنید تا به عامل کمک کنید جریان منطقی عملیات را بشناسد، و این برای مشخص کردن پیشنیازها یا هدایت عامل به سمت مسیرهای بهینه برای تکمیل تکلیف خوب است.
و میتوانید بخشی درباره محدودیتها اضافه کنید که حدود منطقی انواع دادههای ورودی را تعریف کند یا الگوهای عملیاتی را تقویت کند.
اینها اصول اولیه 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 را اعلام میکنید.
و سپس میتوانید یک گرداننده برای AppFunction در واسط کاربر خود پیادهسازی کنید.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}و بخش جالب این است که چرخه حیات گردانندهها بهطور خودکار به چرخه حیات محدوده روال همزمان مورداستفاده شما گره خورده است.
اکنون میخواهید این آداپتور درخواست AppFunctions را ذخیره کنید، ببخشید آداپتور درخواست 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) {
...و میتوانید سطح دسترسی خود یا سیستم یا مورداعتماد را دریافت کنید و از آن استفاده کنید، و این نشان میدهد که کدام عملکردها فقط برای استفاده برای عامل درونبرنامه خودتان است، مانند عاملی که با ML Kit GenAI ساخته شده است، کدام عملکردها فقط برای سیستم دردسترس است، و کدام عملکردها برای عوامل سیستم مورداعتماد دردسترس است.
و برای کمک به شما در ساختن عامل درونبرنامهای خودتان، قبلاً اعلام کردیم و دیدید که ML Kit بهطور بومی از AppFunctions بهعنوان بخشی از چارچوب فراخوانی ابزار یکپارچه خود پشتیبانی میکند.
آنها را یکبار تعریف میکنید و در هر دو عامل سطح سیستم و مدلهای درونبرنامه محلی پوشش دریافت میکنید که به شما امکان میدهد از مواردی مانند اعلامهای ابزار تکهتکهشده اجتناب کنید.
و برای کسب اطلاعات بیشتر، میتوانید این گفتگو را بررسی کنید، «قابلیتهای عاملی را در برنامهتان با ML Kit GenAI باز کنید»، یا میتوانید درواقع فقط بروید و ببینید جدیدترین چیزها در Jetpacker چیست.
خیلی جالب است.
بسیار خوب، برگردیم به سارا.
بسیار خوب.
بنابراین، دن بهتازگی به شما نشان داد که Android MCP چگونه در پشت صحنه کار میکند.
بیایید آن را تمرین کنیم.
بهخاطر داشته باشید که برنامه شما یک سرور است و عاملها کارخواه هستند، بنابراین هدف باید این باشد که مطمئن شوید برنامه شما سرور خوبی برای آنها است.
ما Jetpacker، برنامه نمونه سفرمان، را از اولین اعلام ابزار تا فراخوانی تابع واقعی ازطریق Gemini دنبال خواهیم کرد.
میتوانید AppFunctions را خودتان بنویسید یا از عامل کدنویسی برای شروع کمک بگیرید.
مهارت AppFunctions را منتشر کردهایم که میتواند به شما کمک کند نوشتن توابع خود را شروع کنید و آنها را ازطریق Android CLI به Android Studio متصل کنید یا با عامل کدنویسی انتخابی خود استفاده کنید.
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))
}توضیحات یک جمله تکخطی است که با یک فعل و یک منبع شروع میشود: adds an expense to a trip (هزینهای را به سفر اضافه میکند).
فیلدهای سفر و دسته میتوانند تهی باشند زیرا ممکن است کاربران همیشه این موارد را مشخص نکنند، اما تابع آنها را در بدنه حل خواهد کرد.
و وقتی درخواستی نمیتواند موفق شود، AppFunctionException را برمیگرداند تا عامل متوجه شود چه مشکلی پیش آمده است.
بسیارخوب، حالا بیایید به آزمایش نگاهی بیندازیم.
تابع شما اکنون، توابع شما اکنون سرویسهایی هستند که عوامل میتوانند از آنها استفاده کنند.
بنابراین درواقع دو بخش وجود دارد که باید آزمایش کنید: نحوه رفتار تابع شما و نحوه رفتار عامل.
برای تابع شما، با آزمون واحدی شروع میکنیم که احتمالاً قبلاً نوشتهاید، اما چند نکته وجود دارد که باید درنظر داشته باشید وقتی که عامل واقعاً فراخوانی تابع را انجام میدهد.
اول اینکه مقادیر پیشفرض Kotlin در اینجا اعمال نمیشود.
پارامتر حذفشده بهصورت null، false، یا 0 میرسد.
بنابراین، محدودیتی که قبلاً بهطور پیشفرض ۱۰ بود، در اینجا درواقع ۰ خواهد بود.
تماس نیز در رشته اصلی شروع میشود و فراخوانی تابع شما از یک آزمایش ممکن است همیشه این موضوع را آشکار نکند، بنابراین باید IO استاندارد خود را روی یک توزیعکننده پسزمینه اجرا کنید.
وقتی چیزی ناموفق است، AppFunctionException را نشان دهید.
هر چیز دیگری بهعنوان خطای ناشناخته عمومی با پیام استثنای خام پیوستشده به نماینده شما خواهد رسید.
و ورودیهای شما از یک مدل میآید، بنابراین حتماً باید سناریوهای موارد مرزی مانند جاهای خالی، مترادفهای دلار مانند USD، و مقادیر خارج از محدوده را آزمایش کنید.
برای طرف عامل، باید بدانید که آیا عامل ابزار شما را در زمان مناسب انتخاب میکند و اینکه آیا تابع را بهدرستی اجرا میکند یا نه.
رفتار هر نماینده کمی متفاوت است، بنابراین مهم است که توابع خود را با هر نمایندهای که انتظار دارید از آنها استفاده کند آزمایش کنید.
میتوانید از عامل نمونه متنباز ما برای آزمایش استفاده کنید یا میتوانید از Gemini برای آزمایش این ادغام استفاده کنید.
اگر میخواهید ببینید Gemini چگونه با AppFunctions شما تعامل میکند یا میخواهید Android MCP را در برنامه Gemini راهاندازی کنید، میتوانید این کار را با «پلاتفرم برنامههای متصلشده» انجام دهید که امروز آن را راهاندازی کردیم و درحالحاضر در مرحله پیشنمایش است.
برای کسب اطلاعات بیشتر، میتوانید به گفتگوی قبلی ما درباره گسترش دادن برنامهتان به Gemini و «جستجو» ازطریق «پلاتفرم برنامههای متصلشده» مراجعه کنید.
وقتی عضو برنامه پیشنمایش شدید، فرایند آزمایش با Gemini فقط دو مرحله دارد.
ابتدا، در «کنسول یکپارچه Google»، برنامه متصلشدهای ایجاد میکنیم که Gemini بتواند از آن استفاده کند.
و سپس، دوم، در «کنسول Play»، باید دسته برنامهتان را بارگذاری کنیم تا بتوانیم به AppFunctions دسترسی داشته باشیم و شما بتوانید در دسته هدف آزمایشی داخلی شروع کنید تا آزمایشکنندگان بتوانند واقعاً عملکردها را اعتبارسنجی کنند.
پس بیایید از «کنسول یکپارچه Google» شروع کنیم.
در اینجا، بااستفاده از دکمه «ایجاد برنامه» در بالا سمت چپ، برنامه متصلشدهای ایجاد خواهید کرد.
وقتی برنامه متصلشده را ایجاد میکنید، Android MCP را در این صفحه انتخاب کنید.
اگر درحال ساختن پیادهسازی MCP سمت سرور بودید، اینجا جایی است که کد سرور خود را بارگذاری میکنید.
وقتی روی «راهاندازی» کلیک میکنید، هر برنامه Google Play که با حساب توسعهدهنده شما مرتبط باشد در اینجا نشان داده میشود و آماده است تا آن را به برنامه متصلشدهتان پیوند دهید.
اگر ازقبل برنامه Google Play ندارید، ابتدا باید برنامهای در «کنسول Play» ایجاد کنید.
در اینجا، ما ازقبل Jetpacker را داریم، بنابراین ادامه میدهیم و آن را پیوند میدهیم.
سپس، جزئیاتی را که کاربران هنگام تعامل با برنامه شما در میانای Gemini میبینند اضافه کنید.
این اطلاعات شامل مواردی مثل نماد برنامه، نام نمایشی، قابلیتهای برنامه متصلشده شما، و پیاموارههای نمونهای است که کاربران میتوانند برای تعامل واقعی با آن استفاده کنند.
این موارد باید توصیفی باشند تا به بهترین شکل کاربران را راهنمایی کنند.
بسیار خوب، راهاندازی برنامه متصلشده به پایان رسید، اما شاید برایتان سؤال باشد که برنامه متصلشده چگونه به کارکردها دسترسی پیدا میکند.
چون توابع در کد برنامه شما قرار دارند، اکنون باید به «کنسول Play» برویم و دسته برنامهتان را بارگذاری کنیم و دسته آزمایشی ایجاد کنیم تا بتوانیم آن را با کاربران واقعی آزمایش کنیم.
بنابراین، در این صفحه، بسته برنامه را بارگذاری میکنیم و سپس در اینجا میبینیم که Play میتواند AppFunctions را شناسایی کند و میتوانید آنها را مستقیماً به برنامه متصلشدهتان پیوند دهید.
اکنون میتوانیم برنامه متصلشده Jetpack را بهطور کامل با کارکردهای ادغامشده شما ببینیم.
سپس باید آزمایشکننده اضافه کنیم تا فردی واقعاً آماده باشد که توابع شما را اعتبارسنجی کند و مطمئن شود که بهدرستی کار میکنند.
بنابراین، روی نسخههای برنامه متصلشدهتان در اینجا کلیک کنید.
در این صفحه، میتوانید «مدیریت آزمایشگران» را انتخاب کنید و فهرست محدودی از آزمایشگران را پیکربندی کنید که پساز دردسترس قرار گرفتن برنامه متصل شما در Gemini، بتوانند به آن دسترسی داشته باشند.
میتوانید آزمونگرها را به همان روشی که برای هر نسخه پخش دیگری انجام میدهید به این دسته هدف آزمایشی اضافه کنید.
مطمئن شوید که فهرست آزمایشگران برنامه متصلشده با فهرست آزمایشگران برنامه Google Play شما یکسان باشد.
بسیارخوب، حالا بیایید ادغام را آزمایش کنیم.
این برنامه Jetpacker در تلفن کاربر است که ازقبل سفری برای این اجلاس در آن ثبت شده است.
Gemini را باز میکنم تا برنامه را متصل کنم.
باید مطمئن شوم که همان نسخه برنامهای که قبلاً در Play بارگذاری کردهایم در دستگاه من نصب شده است و باید از حسابی برای Gemini استفاده کنیم که به فهرست آزمایشکنندگان اضافه شده است.
بنابراین در اینجا، Jetpacker را ذکر میکنم و شماره تأیید اتاق هتلم در مانتین ویو را میپرسم.
Gemini با AppFunctions مربوط به Jetpacker در دستگاه تماس میگیرد و پاسخ میدهد: «RED11223 در Redwood Grove Hotel.»
سپس میپرسم: «چقدر بودجه برای ADS باقی مانده است؟»
و فراخوانی تابع ۱۸۵۰ دلار را برمیگرداند.
اکنون میخواهم تغییری ایجاد کنم، بنابراین از Gemini میخواهم ۴۵ دلار برای Uber فرودگاه به سفر ADS من اضافه کند.
Gemini تابع addExpense ما را از قبل فراخوانی میکند تا این را مستقیماً به برنامه سفر من اضافه کند.
و وقتی Jetpacker را باز میکنم، هزینه ازقبل آنجا است و بودجه بههمان نسبت کاهش یافته است.
اگر میخواهید از همین امروز ساختن برنامههای متصلشده برای Gemini را شروع کنید، برای دسترسی به «پلاتفرم برنامههای متصلشده» که درحالحاضر در مرحله پیشنمایش است درخواست دهید.
بسیار خوب.
پس بیایید این را جمعبندی کنیم.
بنابراین، ما در آغاز یک تغییر بزرگ در محاسبات تلفن همراه هستیم و واقعاً زمان آن رسیده است که مطمئن شوید برنامهتان آماده است.
بنابراین، کار را با شناسایی مسیرهای کاربر باارزش اصلی برنامهتان شروع کنید.
بااستفاده از MCP سمت سرور، باطن ابریتان را آشکار کنید و از MCP Android برای کار با دادههای رمزگذاریشده سرتاسر، عملیات محلی، حسگرهای دستگاه، و دیگر موارد درون دستگاه استفاده کنید.
اکنون برای ایجاد نمونه اولیه میتوانید Android Studio و Android CLI را بارگیری کنید.
میتوانید وابستگی AndroidX AppFunctions beta را به فایلهای ساخت خود اضافه کنید و اولین عملکرد دروندستگاهی خود را بهصورت محلی آزمایش کنید.
برای ثبتنام جهت شروع زودهنگام تولید با Gemini، میتوانید ازطریق این رمزینه پاسخسریع در اینجا ثبتنام کنید.
و البته، حتماً این گفتگوهای مرتبط درباره کار با «پلاتفرم برنامههای متصلشده» را بررسی کنید و همچنین ببینید چگونه میتوانید از 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))
}