مدير منتجات في Google
يتطوّر Android ليصبح نظامًا ذكيًا حيث ينسّق الوكلاء مثل Gemini المهام في مختلف التطبيقات. للاستفادة من تجارب مستخدم جديدة والمشاركة في مهام سير العمل المستندة إلى الوكلاء، يجب أن يعرض تطبيقك إمكاناته الأساسية.
هذه الجلسة هي دليل عملي لدمج تطبيقك مع طبقة الذكاء في Android. سنستعرض مسارات الدمج الرئيسية، بما في ذلك بروتوكول MCP على أجهزة Android، وبروتوكول MCP من جهة الخادم، وأتمتة المهام، وسنقدّم إطار عمل لاتخاذ القرار المناسب لحالة الاستخدام لديك. سنستكشف أيضًا التجارب الاستباقية باستخدام "السياق الذكي".
ستتعرّف على كيفية:
AppFunctionDeclaration، وتقديم أوصاف KDoc، وتجنُّب الاعتماد على قيم Kotlin التلقائية.النص
حسنًا.
مرحبًا،
أنا "دان غالبين" من فريق علاقات المطوّرين.
وأنا "سارة شمسي"، مديرة منتجات أعمل على أُطر عمل ذكاء المطوّرين في Android.
يسرّنا جدًا الترحيب بكم في جلسة اليوم.
نشهد تحوّلاً جذريًا في كيفية تفاعل المستخدمين مع أجهزتهم.
في عصر الويب، كان المستخدمون يبدأون رحلاتهم عادةً بالانتقال إلى متصفّح الويب ثم استخدام وظيفة البحث.
وفي عصر الأجهزة الجوّالة، لاحظنا تغيّرًا في سلوك المستخدمين، إذ أصبحوا يستخدمون تطبيقات أصلية أكثر تخصّصًا لإكمال إجراءات لمرة واحدة.
إليك مثال على ذلك.
تصلني رسالة من صديقي يدعوني فيها إلى العشاء.
للردّ على هذه الدعوة، عليّ التحقّق من تقويمي لمعرفة ما إذا كنت متفرّغًا، وإضافة الحدث إلى تقويمي، وحجز رحلة إذا كنت بحاجة إلى وسيلة للوصول إلى هناك، ثم إرسال رسالة إلى صديقي بأنّني أستطيع الحضور.
في السابق، كان ذلك يتطلّب ما يُعرف بالتنقّل بين التطبيقات، حيث كان عليّ التبديل بين تطبيقات مختلفة على جهازي لإكمال الإجراءات وإتمام رحلتي.
أما الآن، فنحن في عصر الذكاء.
في عصر الذكاء الاصطناعي، يبدأ المستخدمون رحلاتهم من خلال التعبير عن نوايا وأهداف عالية المستوى بدلاً من الإجراءات المحددة التي يحتاجون إلى تنفيذها.
هناك تحوّل أساسي في نظرة المستخدمين إلى أجهزتهم، إذ أصبحوا يرونها كأداة تتوقّع احتياجاتهم بشكل استباقي وتساعدهم في تحقيق أهدافهم، بدلاً من مجرّد أداة تتيح لهم تنفيذ الإجراءات.
ولاحظنا أنّهم يتوقّعون أن يعمل إطار العمل الذكي هذا ليس فقط على أجهزتهم الجوّالة، بل على منظومة متكاملة من الأجهزة ذات التصاميم المتنوعة، بما في ذلك الساعات والسيارات وأجهزة التلفزيون وأجهزة الكمبيوتر المحمولة وأجهزة الواقع الممتد والنظارات.
في عصر الذكاء الاصطناعي، يمكن لطلب واحد يشمل تطبيقات متعددة أن يسهّل تنفيذ مثال دعوة العشاء المذكور سابقًا.
بطلب واحد فقط، يمكن لوكيل على جهازي المساعدة في تفعيل جميع الإجراءات التي أحتاجها، بدءًا من التحقّق من تقويمي وإضافة الحدث وحجز الرحلة وإرسال رسالة، كل ذلك دفعة واحدة.
سنشارك اليوم المزيد من المعلومات حول خطط Android في هذا المجال، ثم سنوضّح كيف يمكنكم كمطوّرين إنشاء تجارب من الدرجة الأولى.
دان:
شكرًا، سارة.
لذلك، يواصل Android إضافة الذكاء الاصطناعي في جوهره، ما يوفّر طرقًا جديدة لتطبيقاتك للاستعداد لعصر الذكاء الاصطناعي هذا.
الركيزة الأولى من ركيزتَي ذكاء المطوّرين هي الذكاء داخل التطبيق، وهو يتعلّق بجعل تطبيقاتك أكثر ذكاءً، وإدخال الذكاء إلى تطبيقك، ويمكنك استخدام النماذج على الجهاز فقط، مثل Gemini Nano 4، من خلال حزمة تعلّم الآلة لتلخيص سلاسل الرسائل وإعادة كتابة الردود وفهم الصور، أو استخدام اللغة الطبيعية لاستدعاء وظائف تطبيقك.
يمكنك أيضًا استخدام نماذج Gemini في السحابة الإلكترونية مباشرةً في تطبيقك من خلال Firebase AI Logic.
لكننا سنركّز اليوم على الركيزة الثانية، وهي التأكّد من أنّ إمكانات تطبيقك متاحة ضمن نظام الذكاء الاصطناعي.
إذًا، ما هو النظام الذكي؟
لذلك، بالإضافة إلى التحدّث عن التجارب المستندة إلى الطلبات التي تحدّثت عنها "سارة" سابقًا واستضافتها، هو نظام يتيح عمليات دمج عميقة لتحسين التجارب في منظومة التطبيقات المتكاملة.
لذلك، تستفيد الأجهزة المصمّمة لذكاء Gemini، على سبيل المثال، من نقاط التكامل هذه لتقديم ميزات مثل الاقتراحات والإشعارات الملخّصة والإدخال الصوتي المحسّن والملء التلقائي للنماذج.
أصبحت الاقتراحات الآن جزءًا من ميزة تُعرف باسم "المساعدة الاستباقية".
حتى الآن، كنا نتحدث عن المساعدة المستندة إلى طلبات.
نحن جميعًا معتادون على ذلك، حيث يطلب المستخدم من الوكيل إنجاز مهمة واحدة أو أكثر نيابةً عنه.
لكن المساعدة الاستباقية تتعلق في الواقع بتوقع الإجراء أو الإجراءات التالية التي قد يتخذها المستخدم.
بالعودة إلى مثال العشاء، قد يقدّم الوكيل للمستخدم اقتراحات استباقية ردًا على الرسالة، مثل إضافة الدعوة إلى التقويم أو حجز رحلة مشتركة.
ولكن كيف يطلب من المستخدم ذلك؟
يمكن أن تعرض ميزة "المساعدة الاستباقية" الإجراءات في أماكن مثل شاشة القفل والإشعارات ولوحات المفاتيح الافتراضية وحتى داخل تطبيقات محدّدة.
إذًا، كيف يمكنك جعل تطبيقك يظهر فعليًا في هذه الشرائح الاستباقية لجذب الزيارات إلى تطبيقك؟
حسنًا، أنت تتّخذ بالفعل إجراءات مثل إنشاء التطبيقات باستخدام أحدث إصدار من حزمة تطوير البرامج (SDK) وأحدث أدواتنا، ولكن أفضل ما يمكنك فعله الآن هو التأكّد من أنّ تطبيقك جاهز أيضًا للمساعدة المستندة إلى الطلبات، وهو ما ستتحدّث عنه "سارة" بعد قليل.
شكرًا، دان.
حسنًا، المساعدة المستندة إلى الطلبات تتضمن توجّه المستخدمين مباشرةً إلى وكيل وطلب منه بدء المهام وتنظيمها، حتى على مستوى التطبيقات.
ويمكن أن يؤدي ذلك إلى أتمتة مهام سير العمل التي كانت تتم يدويًا وتستغرق وقتًا طويلاً.
في نظام الذكاء الاصطناعي على Android، نركّز بشكل أساسي على برامج النظام، مثل Gemini، التي يمكنها الوصول إلى التطبيقات على جهاز المستخدم.
هناك بضع طرق مختلفة يمكن أن يظهر بها تطبيقك في هذه التجربة ويتفاعل مع وكيل النظام.
هناك حلّ "أتمتة المهام" المستنِد إلى المنصة، وهناك عمليات التكامل المباشر لواجهة برمجة التطبيقات في وكيل النظام.
ننصح دائمًا بالإنشاء باستخدام عمليات دمج مباشرة لواجهة برمجة التطبيقات كلما أمكن ذلك.
يتحكّم المطوّرون في هذه الإعدادات، لذا يمكنك تحديد طريقة ظهور تطبيقك وعلامتك التجارية والتحكّم فيها.
وهي أيضًا أسرع وأكثر موثوقية، ما يوفّر تجربة أفضل للمستخدمين.
هناك مساران لواجهة برمجة التطبيقات يمكنك استخدامهما عند التكامل مع الوكيل.
"بروتوكول سياق النموذج" (Model Context Protocol)، أو MCP اختصارًا، هو أفضل ممارسة في المجال لاستدعاء الدوال من جهة الخادم.
أما بالنسبة إلى استدعاء الأدوات على الجهاز فقط، فنعلن رسميًا عن Android MCP، وهو إطار عمل موحّد لاستدعاء الدوال على Android.
يستند بروتوكول MCP على Android إلى مكتبة AppFunctions في Jetpack، والتي تتوافق بشكل وثيق مع بروتوكول MCP من الناحيتين الفنية والمفاهيمية، مع إتاحة استخدام لغة Kotlin المألوفة والاصطلاحية التي يعرفها العديد من مطوّري برامج Android.
وهذا يجعل تنفيذ واجهة برمجة التطبيقات بديهيًا وسهل الاستخدام نسبيًا، ويمكنك الاستفادة من أي معلومات قد تكون لديك من عملية دمج MCP من جهة الخادم التي تم إعدادها مسبقًا.
تتوافق العديد من عناصر MCP الأساسية مع بنية Jetpack AppFunctions.
لنطّلِع على بعض الأمثلة لتوضيح ذلك.
تتوافق نقطة نهاية أداة MCP بشكل جيد مع دالة Kotlin التي تحتوي على التعليق التوضيحي AppFunctionDeclaration.
يتم ربط نماذج pydantic بفئات بيانات Kotlin التي تم وضع تعليقات توضيحية عليها باستخدام AppFunctionSerializable.
ولكن بدلاً من استخدام بروتوكول HTTP القابل للبث أو بروتوكول نقل البيانات العادي لوحدات الإدخال والإخراج، يعمل بروتوكول Android MCP على بروتوكول Android Binder IPC المحسَّن.
يسرّنا اليوم الإعلان عن أنّ مكتبة Jetpack، AppFunctions، أصبحت الآن في مرحلة الإصدار التجريبي.
أنا متأكد من أنّكم تتساءلون الآن عن الحالات التي يكون فيها من الأفضل استخدام مسار واحد لواجهة برمجة التطبيقات، والحالات التي يكون فيها من الأفضل استخدام Android MCP تحديدًا.
بشكل عام، يكون بروتوكول Android MCP مفيدًا بشكل خاص عندما تريد الاستفادة من السياق المحلي أو على الجهاز فقط لإكمال تجربة المستخدم.
لنتعرّف على بعض الأمثلة التي يتألق فيها Android MCP.
على وجه التحديد، يجب استخدام Android MCP عندما تحتاج إلى الوصول إلى البيانات المحلية المشفّرة.
وهذا مهم بشكل خاص لتطبيقات المراسلة والتواصل، حيث يجب أن يظل سجلّ الرسائل على الجهاز فقط بسبب التشفير التام بين الأطراف.
تُعدّ Android MCP مفيدة أيضًا للإجراءات التي تتم على الجهاز فقط وبدون إنترنت، إذا كان الوكلاء يتيحون ذلك.
أطلق شريكنا في Wear OS، وهو Samsung Health، وظيفة لبدء الجري وبدء التمارين، ويتم تنظيمها بالكامل على الجهاز فقط.
غالبًا ما يريد المستخدمون تنفيذ هذه الأنواع من الإجراءات بسرعة فائقة وموثوقية عالية ووقت استجابة منخفض، وأحيانًا حتى في بيئات الاتصال الضعيف، ويضمن ذلك التأكّد من أنّ عملية استدعاء الأدوات وتوجيهها وتوجيه طلبات البحث بالكامل يمكن أن تتم على الجهاز فقط.
يُعدّ بروتوكول MCP على Android أيضًا مسار واجهة برمجة التطبيقات المفضّل إذا أردت الوصول إلى أدوات استشعار الجهاز أو التفاعل مباشرةً مع أجهزته.
على سبيل المثال، تهدف العديد من تطبيقات السيارات إلى السماح للمستخدمين بفتح أبواب سياراتهم على الفور من خلال إمكانية الاتصال عبر النطاق الفائق العرض (UWB) وبدون لمس الجهاز في كثير من الأحيان.
ويمكن أن تساعد ميزة Android MCP في ذلك.
وأخيرًا، هو الحل المناسب إذا لم تكن هناك نقاط النهاية في السحابة الإلكترونية متاحة لتطبيقاتك أو واجهة برمجة التطبيقات.
العديد من تطبيقات الطرف الأول التي توفّرها الشركات المصنّعة للأجهزة هي تطبيقات من جهة العميل فقط، ولهذا السبب تستخدم هذه الشركات Android MCP.
على سبيل المثال، يمكن استخدام Gemini للبحث عن الصور واستردادها من تطبيقات معرض الصور التي تعمل من جهة العميل فقط، مثل Xiaomi Gallery.
هنا، يمكنني فتح Gemini وطلب "العثور على صور روبوت Android التي التقطتها الأسبوع الماضي في معرض Xiaomi".
أتاحت Xiaomi وظيفة في التطبيق للبحث عن الصور، والتي يستدعيها Gemini بعد ذلك، ويتم عرض الصور ذات الصلة مباشرةً في واجهة مستخدم تطبيق Gemini، وبالتالي لا يحتاج المستخدم إلى مغادرة التطبيق.
ونعتمد على البنية نفسها لتنفيذ الإجراءات على الجهاز فقط في نظام التشغيل Android.
يوفّر بروتوكول MCP على Android ميزة "مساعدة الجهاز" التي تم إطلاقها مؤخرًا، وهي عبارة عن دمج مباشر لنظام التشغيل Android في تطبيق Gemini.
تم إطلاق هذه الميزة على أجهزة Pixel وLenovo، ويتم حاليًا توسيع نطاقها لتشمل منظومة المصنّعين الأصليين لأجهزة Android على نطاق أوسع.
يمكن للمستخدمين استخدام ميزة "مساعدة الجهاز" من خلال الانتقال إلى Gemini وسؤاله عن كيفية تحسين إعداداتهم وإعدادات أجهزتهم وإعدادات النظام لتحسين تجربة استخدام أجهزتهم.
على سبيل المثال، بدلاً من البحث في قوائم مختلفة، يمكن لمستخدم مصاب بعمى الألوان أن يسأل Gemini: "أنا مصاب بعمى الألوان.
كيف يمكنني استخدام هذا الهاتف بشكل أفضل؟"
سيستدعي Gemini وظائف التطبيق على الجهاز ويحلّل الطلب بذكاء ويقترح طرقًا فعلية يمكن للمستخدم من خلالها تغيير إعدادات النظام، وطرقًا محدّدة يمكنه من خلالها تغيير إعدادات الجهاز لتحسين التجربة.
بعد ذلك، تظهر عناصر التحكّم داخل التطبيق مباشرةً في واجهة مستخدم تطبيق Gemini، ما يتيح للمستخدمين التحكّم في الإجراءات فورًا.
على سبيل المثال، عند تلبية طلب تحسين تجربة المستخدمين المصابين بعمى الألوان، عرض Gemini زر تفعيل تصحيح الألوان، ما يتيح للمستخدمين إجراء التغيير على الفور.
تتوفّر تجارب MCP على Android حاليًا على أجهزة محدّدة، ونعمل مع شركاء متعدّدين لتوفير المزيد من حالات الاستخدام في منظومة Android المتكاملة.
مع أنّ التصريحات الصريحة لأداة MCP في Android هي المعيار الذهبي، نعلم أنّه ستكون هناك حالات يطلب فيها المستخدمون من الوكلاء إكمال حالات استخدام معقّدة أو أقل شيوعًا، وهنا يأتي دور ميزة "أتمتة المهام" المستندة إلى المنصة.
بموجب هذا النموذج، يمكن للوكيل المحمَّل مسبقًا من المصنّع الأصلي للجهاز استخدام واجهات برمجة التطبيقات للتحكّم في الكمبيوتر على Android لمحاكاة أحداث اللمس على واجهة التطبيق.
لا يتطلّب ذلك من المطوّرين إجراء أي تغييرات على الرموز البرمجية، بل يتطلّب من الوكيل النقر على واجهة المستخدم كما يفعل المستخدم، وذلك من خلال تحليل شاشة التطبيق.
لضمان تحكّم المستخدمين وأمانهم، من الضروري أن يتوقف الوكلاء مؤقتًا عند تنفيذ إجراءات قد تكون حسّاسة، مثل عمليات الدفع، وأن يطلبوا تأكيدًا من المستخدم للمتابعة.
لنلقِ نظرة على مثال عملي.
في هذا المثال، التقط مستخدم صورة لجولة تذوّق القهوة والشوكولاتة، ويريد أن يطلب من Expedia أو أن يسأل Gemini البحث في Expedia عن جولة مماثلة، ولكن مع تعديل عدد المشاركين إلى ستة.
بدون الحاجة إلى دمج واجهة برمجة التطبيقات مباشرةً، يتنقّل Gemini في واجهة مستخدم Expedia في الخلفية بالطريقة نفسها التي يتنقّل بها المستخدم.
بعد ذلك، يعرض الخيارات ذات الصلة التي عثر عليها وينقلها بسلاسة إلى المستخدم عندما يحين وقت التأكيد النهائي والحجز.
من المهم أن ندرك أنّ هذه المسارات الثلاثة المختلفة ليست متعارضة، بل هي في الواقع أفضل معًا.
يجب أن يتوقّع المستخدمون والمطوّرون أن تتمكّن الوكلاء من تنسيق الأدوات عبر عمليات تنفيذ الخادم، ثم تطبيق Android المحلي أيضًا لتقديم تجربة أكثر شمولاً وتكاملاً للمستخدمين.
حسنًا.
شكرًا لك، سارة.
سنتعمّق أكثر في MCP على Android.
وعندما يكون ذلك ممكنًا، أريدكم أن تعرفوا أنّني أسحب مقتطفات رمز MCP من Jetpacker، وهو نموذج تطبيق السفر الذي تم إنشاؤه بمساعدة Antigravity.
لقد رأيتم ذلك في الكلمة الرئيسية.
وهو مزوّد بجميع أنواع الميزات الذكية الرائعة، بما في ذلك عمليات الدمج على الجهاز فقط مع واجهات برمجة التطبيقات حزمة تعلّم الآلة للذكاء الاصطناعي التوليدي وحزمة تعلّم الآلة للتعرّف على الكلام.
أصبح الآن بروتوكول 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 على "صحيح"، يقدّم 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 تمرّر قيمة فارغة للمَعلمات التي يمكن أن تكون فارغة والتي تم حذفها.
لن أعرض حتى مثالاً على هذه الطريقة الأولى، ولكن لا تستخدموا مَعلمات شاملة.
فالسياق الكامل أو سجلّ المحادثات مثلاً لا يظهران بشكل جيد.
فقد تؤدي إلى تمرير الوكلاء لكميات كبيرة من البيانات واستخراجها، وهي ممارسة سيئة.
بما أنّ 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 واحدة أو أكثر مسجّلة لتلبية طلب المستخدم.
لنتحدّث الآن عن 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 ثم إعادة قراءة الأوصاف مع AppFunctions الفردية.
لذلك، يجب التركيز في بداية الوصف على تقديم شرح موجز للمجال الأساسي لتطبيقك أو أدواتك.
ويمكنك بعد ذلك إضافة أنماط تشغيلية لمساعدة الوكيل في فهم التسلسل المنطقي للعمليات، وهذا مفيد لتحديد المتطلبات الأساسية أو توجيه الوكيل نحو المسارات المثلى لإكمال المهام.
ويمكنك إضافة قسم عن القيود يحدّد الحدود المنطقية لأنواع بيانات الإدخال أو يعزّز الأنماط التشغيلية.
هذه هي أساسيات "برنامج صانعي المحتوى على Android"، ولكن هناك بعض الأمور الإضافية التي يمكنك تنفيذها.
من الميزات الرائعة التي يمكنك الاستفادة منها هي تفعيل واجهات 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) {
...ويمكنك الحصول على مستوى الوصول الذاتي أو النظام أو الموثوق به واستخدامه، ويشير ذلك إلى الوظائف المخصّصة للاستخدام فقط مع وكيلك داخل التطبيق، مثل الوظائف التي تم إنشاؤها باستخدام حزمة تعلّم الآلة GenAI، والوظائف المتاحة للنظام فقط، والوظائف المتاحة لوكلاء النظام الموثوق بهم.
ولمساعدتكم في إنشاء وكيل خاص بكم داخل التطبيق، أعلنّا سابقًا أنّ "حزمة تعلّم الآلة" تتيح استخدام AppFunctions كجزء من إطار عملها الموحّد لاستدعاء الأدوات.
يمكنك تحديدها مرة واحدة والحصول على تغطية على مستوى كل من الوكلاء على مستوى النظام والنماذج المحلية داخل التطبيق، ما يتيح لك تجنُّب تعريفات الأدوات المجزأة.
لمزيد من المعلومات، يمكنك الاطّلاع على هذه المحاضرة بعنوان "الاستفادة من إمكانات الذكاء الاصطناعي الوكيل في تطبيقك باستخدام حزمة تعلّم الآلة والذكاء الاصطناعي التوليدي"، أو يمكنك الاطّلاع على آخر الأخبار في Jetpack.
هذا رائع جدًا.
حسنًا، لنعد إلى سارة.
حسنًا.
لقد شرح لكم "دان" للتو طريقة عمل Android MCP من الداخل.
لنطبّق ذلك عمليًا.
تذكَّر أنّ تطبيقك هو خادم والوكلاء هم العميل، لذا يجب أن يكون الهدف هو التأكّد من أنّ تطبيقك خادم جيد لهم.
سنستخدم Jetpacker، وهو نموذج تطبيق السفر، بدءًا من أول تعريف للأداة وصولاً إلى استدعاء دالة فعلي من خلال Gemini.
يمكنك كتابة دوال AppFunctions يدويًا أو استخدام وكيل ترميز لمساعدتك في البدء.
لقد أطلقنا مهارة AppFunctions التي يمكن أن تساعدك في البدء في كتابة الدوال التي يمكنك ربطها بـ استوديو Android من خلال 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 التلقائية هنا.
تصل المَعلمة المحذوفة كقيمة فارغة أو خطأ أو 0.
وبالتالي، فإنّ الحدّ الأقصى الذي كان مضبوطًا تلقائيًا على 10 سيصبح 0 هنا.
يبدأ الاستدعاء أيضًا في سلسلة التعليمات الرئيسية، وقد لا يكون استدعاء الدالة من اختبار واضحًا دائمًا، لذا يجب تشغيل عمليات الإدخال والإخراج العادية على مُرسِل في الخلفية.
عند حدوث خطأ، يتم عرض AppFunctionException.
أما أي خطأ آخر، فسيصل إلى وكيلك كخطأ عام غير معروف مع إرفاق رسالة الاستثناء الأولية.
وستأتي مدخلاتك من نموذج، لذا يجب بالتأكيد اختبار سيناريوهات الحالات الحدية مثل الفراغات والمرادفات للدولار مثل USD والقيم خارج النطاق.
بالنسبة إلى الوكيل، عليك معرفة ما إذا كان يختار الأداة في الوقت المناسب وينفّذ الدالة بشكل صحيح.
يختلف سلوك كل وكيل قليلاً عن الآخر، لذا من المهم اختبار الدوال مع أي وكيل تتوقع استخدامه لها.
يمكنك استخدام نموذج الوكيل المفتوح المصدر أو Gemini لاختبار عملية الدمج هذه.
إذا أردت الاطّلاع على كيفية تفاعل Gemini مع AppFunctions أو أردت إطلاق بروتوكول MCP على Android في تطبيق Gemini، يمكنك إجراء ذلك من خلال "منصة التطبيقات المرتبطة" التي أطلقناها في وقت سابق من اليوم، وهي متاحة حاليًا كنسخة حصرية.
يمكنك معرفة المزيد من خلال الاطّلاع على حديثنا السابق حول توسيع نطاق تطبيقك ليشمل Gemini و"بحث Google" من خلال "منصة التطبيقات المرتبطة".
بعد الانضمام إلى برنامج المعاينة، تتألف عملية الاختبار باستخدام Gemini من خطوتَين.
أولاً، في "وحدة التحكّم الشاملة من Google"، سننشئ تطبيقًا مرتبطًا يمكن لـ Gemini استخدامه.
ثانيًا، في Play Console، يجب تحميل حزمة تطبيقك لنتمكّن من الوصول إلى AppFunctions، ويمكنك البدء في مسار اختبار داخلي ليتمكّن المختبِرون من التحقّق من صحة الدوال.
لنبدأ الآن في "وحدة التحكّم الشاملة من Google".
هنا، ستنشئ تطبيقًا مرتبطًا باستخدام الزر "إنشاء تطبيق" (Create an app) في أعلى يسار الصفحة.
عند إنشاء التطبيق المرتبط، اختَر Android MCP في هذه الشاشة.
إذا كنت بصدد إنشاء عملية تنفيذ MCP من جهة الخادم، يمكنك تحميل رمز الخادم هنا.
عند النقر على "إعداد"، ستظهر هنا أي تطبيقات على Google Play مرتبطة بحساب المطوّر الخاص بك، وستكون جاهزة للربط بتطبيقك المرتبط.
إذا لم يكن لديك تطبيق على Google Play، عليك إنشاء تطبيق أولاً في Play Console.
لدينا هنا Jetpacker، لذا سنربطه.
بعد ذلك، أضِف التفاصيل التي ستظهر للمستخدمين في واجهة Gemini الأمامية عندما يحاولون التفاعل مع تطبيقك.
ويشمل ذلك رمز التطبيق واسمه المعروض وإمكاناته وأمثلة على الطلبات التي يمكن للمستخدمين توجيهها إليه للتفاعل معه.
يجب أن تكون هذه الأوصاف وصفية للمساعدة في توجيه المستخدمين على أفضل وجه.
بهذا نكون قد انتهينا من إعداد التطبيق المرتبط، ولكن قد تتساءلون عن كيفية وصول التطبيق المرتبط إلى الدوال.
بما أنّ الدوال مضمّنة في رمز تطبيقك، سنحتاج الآن إلى الانتقال إلى Play Console وتحميل حِزمة تطبيقك وإنشاء مسار اختبار، حتى نتمكّن من اختباره مع مستخدمين حقيقيين.
لذا، سنحمّل حزمة التطبيق على هذه الشاشة، وبعدها سنرى هنا أنّ Google Play يمكنه رصد AppFunctions، ويمكنك ربطها مباشرةً بتطبيقك المرتبط.
يمكننا الآن رؤية التطبيق المرتبط بـ Jetpack مكتملًا مع دمج وظائفك.
بعد ذلك، سنحتاج إلى إضافة مختبِرين، ليكون هناك شخص جاهز فعليًا للتحقّق من صحة الدوال والتأكّد من أنّها تعمل بشكل صحيح.
لذا، انقر على إصدارات التطبيقات المرتبطة هنا.
على هذه الشاشة، يمكنك اختيار "إدارة المختبِرين" وضبط قائمة محدودة من المختبِرين الذين سيتمكّنون من الوصول إلى تطبيقك المرتبط في Gemini عندما يصبح متاحًا.
يمكنك إضافة مختبِرين إلى مسار الاختبار هذا بالطريقة نفسها التي تتّبعها مع أي إصدار آخر.
تأكَّد من أنّ قائمة المختبِرين للتطبيق المرتبط هي نفسها قائمة المختبِرين لتطبيقك على Google Play.
حسنًا، لنختبر الآن عملية الدمج.
إليك تطبيق Jetpacker على هاتف أحد المستخدمين، وقد تم تسجيل رحلة لهذه القمة.
سأفتح Gemini لربط التطبيق.
يجب التأكّد من أنّ إصدار التطبيق المثبّت على جهازي هو نفسه الذي حمّلناه على Play سابقًا، ويجب استخدام حساب Gemini تمت إضافته إلى قائمة المختبِرين.
لذا، سأذكر Jetpacker هنا وأطلب رقم تأكيد حجز غرفتي في الفندق في ماونتن فيو.
يستدعي Gemini دوال AppFunctions الخاصة بـ Jetpacker على الجهاز ويجيب: "RED11223 في فندق Redwood Grove".
بعد ذلك، سأسأل: "ما هي الميزانية المتبقية لديّ لـ ADS؟"
وسيعرض استدعاء الدالة 1850 دولار.
الآن، أريد إجراء تغيير، لذا سأطلب من Gemini إضافة 45 دولار أمريكي لرحلة Uber إلى المطار إلى رحلتي إلى أبوظبي.
يستدعي Gemini دالة addExpense التي استخدمناها سابقًا لإضافة هذا النشاط مباشرةً إلى برنامج الرحلة.
وعندما أفتح Jetpacker، تكون النفقات موجودة بالفعل، ويتم تخفيض الميزانية وفقًا لذلك.
إذا أردت البدء في إنشاء تطبيقات مرتبطة لـ Gemini اليوم، يمكنك تقديم طلب للوصول إلى "منصة التطبيقات المرتبطة"، وهي متاحة حاليًا كنسخة حصرية.
حسنًا.
لنلخّص ما ذكرناه.
نحن في بداية تحوّل هائل في مجال الحوسبة على الأجهزة الجوّالة، وقد حان الوقت للتأكّد من أنّ تطبيقك جاهز.
لذا، ابدأ بتحديد تجارب المستخدمين الأساسية العالية القيمة في تطبيقك.
يمكنك عرض الواجهة الخلفية للسحابة الإلكترونية باستخدام بروتوكول MCP من جهة الخادم، واستخدام بروتوكول MCP من Android للتعامل مع البيانات المشفّرة بشكل تام بين الأطراف والعمليات المحلية وأدوات الاستشعار في الجهاز وغيرها من العناصر على الجهاز.
لإنشاء نموذج أوّلي الآن، يمكنك تنزيل "استوديو Android" وAndroid CLI.
يمكنك إضافة تبعية الإصدار التجريبي من AndroidX AppFunctions إلى ملفات الإصدار، واختبار أول وظيفة على الجهاز فقط محليًا.
للتسجيل في برنامج الوصول المبكر إلى Gemini، يمكنك الاشتراك من خلال رمز الاستجابة السريعة هذا.
ولا تنسَ الاطّلاع على هذه الجلسات ذات الصلة حول العمل مع "منصة التطبيقات المرتبطة"، وكيفية استخدام AppFunctions الداخلية مع "حزمة تعلّم الآلة" أو مع وكيلك الخاص لتشغيل وكيلك داخل التطبيق.
وأخيرًا، شكرًا جزيلاً على وقتك.
نتطلّع إلى رؤية جميع التجارب الرائعة التي تنشئها لنظام 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))
}