Nhà quản lý sản phẩm tại Google
Android đang phát triển thành một hệ thống thông minh, nơi các tác nhân như Gemini điều phối các nhiệm vụ trên nhiều ứng dụng. Để mang đến trải nghiệm người dùng mới và tham gia vào các quy trình làm việc dựa trên tác nhân này, ứng dụng của bạn phải hiển thị các khả năng cốt lõi.
Phiên này là hướng dẫn thực tế về cách tích hợp ứng dụng của bạn với lớp thông minh của Android. Chúng ta sẽ tìm hiểu các lộ trình tích hợp chính, bao gồm MCP Android trên thiết bị, MCP phía máy chủ và Tự động hoá tác vụ, đồng thời cung cấp một khung quyết định để chọn lộ trình phù hợp cho trường hợp sử dụng của bạn. Chúng ta cũng sẽ khám phá các trải nghiệm chủ động với tính năng Bối cảnh cá nhân.
Bạn sẽ tìm hiểu cách:
AppFunctionDeclaration, cung cấp nội dung mô tả KDoc và tránh dựa vào các giá trị mặc định của Kotlin.Bản chép lời
Được rồi.
Chào các bạn!
Tôi là Dan Galpin, thuộc nhóm Quan hệ với nhà phát triển.
Còn tôi là Sarah Shamsie, nhà quản lý sản phẩm phụ trách các khung thông tin dành cho nhà phát triển của Android.
Chúng tôi rất vui được chào đón bạn đến với buổi hôm nay.
Chúng ta đang chứng kiến một sự thay đổi mang tính đột phá về cách người dùng tương tác với thiết bị của họ.
Trong Kỷ nguyên web, người dùng thường bắt đầu hành trình bằng cách truy cập vào trình duyệt web rồi sử dụng chức năng tìm kiếm.
Sau đó, trong Kỷ nguyên di động, chúng ta thấy hành vi của người dùng chuyển sang sử dụng các ứng dụng gốc, chuyên biệt hơn để hoàn thành các hành động một lần.
Dưới đây là một ví dụ.
Tôi nhận được tin nhắn của một người bạn mời tôi đi ăn tối.
Để trả lời, tôi cần kiểm tra lịch để xem mình có rảnh không, thêm sự kiện vào lịch, đặt xe nếu cần phương tiện di chuyển, rồi nhắn tin lại cho bạn tôi rằng tôi có thể tham dự.
Trước đây, để làm được việc này, tôi phải chuyển đổi qua lại giữa các ứng dụng trên thiết bị để hoàn tất hành động và hoàn thành hành trình của mình.
Nhưng giờ đây, chúng ta đang ở Kỷ nguyên trí tuệ.
Trong Kỷ nguyên trí tuệ, người dùng bắt đầu hành trình của mình bằng cách thể hiện ý định và mục tiêu cấp cao thay vì những hành động cụ thể mà họ cần thực hiện.
Có một sự thay đổi căn bản trong cách họ nhìn nhận thiết bị của mình: không chỉ là một công cụ hỗ trợ hành động, mà còn là một trợ thủ chủ động dự đoán nhu cầu và giúp họ đạt được mục tiêu.
Chúng tôi cũng nhận thấy rằng người dùng mong đợi khung thông minh này không chỉ hoạt động trên thiết bị di động mà còn trên một hệ sinh thái đa dạng về kiểu dáng, bao gồm cả đồng hồ, ô tô, TV, máy tính xách tay, XR và kính.
Trong Kỷ nguyên thông minh, một câu lệnh duy nhất trên nhiều ứng dụng thực sự có thể hỗ trợ ví dụ về lời mời ăn tối trước đó.
Chỉ với một câu lệnh, một tác nhân trên thiết bị của tôi có thể giúp thực hiện tất cả những việc tôi cần, từ kiểm tra lịch, thêm sự kiện, đặt xe cho đến gửi tin nhắn, tất cả chỉ trong một lần.
Hôm nay, chúng tôi sẽ chia sẻ thêm về kế hoạch của Android trong lĩnh vực này và cách các nhà phát triển có thể xây dựng trải nghiệm hàng đầu.
Dan.
Cảm ơn Sarah.
Vì vậy, Android đang tiếp tục bổ sung trí tuệ nhân tạo vào cốt lõi của mình, và điều đó mang đến những cách mới để các ứng dụng của bạn, bạn biết đấy, trang bị cho Kỷ nguyên Trí tuệ này.
Và trụ cột đầu tiên trong hai trụ cột về trí tuệ nhân tạo dành cho nhà phát triển là trí tuệ nhân tạo trong ứng dụng. Trụ cột này rõ ràng là giúp ứng dụng của bạn thông minh hơn, đưa trí tuệ nhân tạo vào ứng dụng của bạn. Bạn có thể sử dụng các mô hình trên thiết bị như Gemini Nano 4 thông qua Bộ công cụ học máy để tóm tắt các chuỗi tin nhắn, viết lại câu trả lời và hiểu hình ảnh, hoặc sử dụng ngôn ngữ tự nhiên để gọi các hàm của ứng dụng.
Bạn thậm chí có thể sử dụng các mô hình Gemini trên đám mây ngay trong ứng dụng của mình thông qua Firebase AI Logic.
Nhưng hôm nay, chúng ta sẽ tập trung vào trụ cột thứ hai, đó là đảm bảo các chức năng của ứng dụng có thể hoạt động trong hệ thống thông minh.
Vậy hệ thống thông minh là gì?
Vì vậy, ngoài việc trò chuyện và lưu trữ những trải nghiệm được nhắc đến mà Sarah đã nói trước đó, đây là một hệ thống cho phép tích hợp sâu để cải thiện trải nghiệm trên toàn hệ sinh thái ứng dụng.
Vì vậy, các thiết bị được thiết kế cho Gemini Intelligence, chẳng hạn như, tận dụng các điểm tích hợp này để cung cấp những tính năng như đề xuất, thông báo tóm tắt, nhập liệu bằng giọng nói được cải thiện và tự động điền biểu mẫu.
Giờ đây, các đề xuất là một phần của tính năng trợ giúp chủ động.
Và cho đến nay, chúng ta đã nói về tính năng hỗ trợ theo câu lệnh.
Chúng ta đều quen với việc người dùng yêu cầu một tác nhân hoàn thành một hoặc nhiều nhiệm vụ thay mặt họ.
Nhưng trên thực tế, trợ giúp chủ động là dự đoán hành động tiếp theo mà người dùng có thể thực hiện.
Vì vậy, quay lại ví dụ về bữa tối, tác nhân có thể chủ động nhắc người dùng bằng các đề xuất để phản hồi tin nhắn, có thể là thêm lời mời vào lịch hoặc đặt xe chung.
Nhưng làm thế nào để nhắc người dùng?
Tính năng trợ giúp chủ động có thể hiển thị các hành động ở những nơi như màn hình khoá, thông báo, bàn phím ảo và thậm chí trong một số ứng dụng.
Vậy làm cách nào để ứng dụng của bạn thực sự xuất hiện trong các khối chủ động này nhằm thu hút lưu lượng truy cập?
Bạn đã làm những việc như xây dựng ứng dụng dựa trên phiên bản SDK mới nhất và xây dựng bằng các công cụ hiện đại nhất của chúng tôi, nhưng điều tốt nhất bạn có thể làm lúc này là đảm bảo ứng dụng của bạn cũng sẵn sàng cho tính năng hỗ trợ theo câu lệnh mà Sarah sẽ nói đến tiếp theo.
Cảm ơn Dan.
Được rồi, vậy thì trợ lý theo câu lệnh là khi người dùng trực tiếp tìm đến một tác nhân và yêu cầu tác nhân đó bắt đầu và điều phối các nhiệm vụ, thậm chí là trên nhiều ứng dụng.
Điều này có thể tự động hoá các quy trình công việc thủ công và tốn thời gian trước đây.
Đối với hệ thống thông minh trên Android, chúng tôi chủ yếu tập trung vào các tác nhân hệ thống như Gemini có thể khai thác các ứng dụng trên thiết bị của người dùng.
Có một vài cách để ứng dụng của bạn xuất hiện trong trải nghiệm này và tương tác với tác nhân hệ thống.
Chúng tôi có giải pháp Tự động hoá tác vụ dựa trên nền tảng và giải pháp tích hợp API trực tiếp vào tác nhân hệ thống.
Bạn nên xây dựng bằng cách tích hợp API trực tiếp bất cứ khi nào có thể.
Những thành phần này do nhà phát triển kiểm soát, vì vậy, bạn có thể quyết định và kiểm soát cách ứng dụng cũng như thương hiệu của bạn xuất hiện.
Các ứng dụng này cũng nhanh hơn và đáng tin cậy hơn, mang đến trải nghiệm cao cấp hơn cho người dùng.
Có hai đường dẫn API mà bạn có thể cân nhắc khi muốn tích hợp với tác nhân.
Giao thức ngữ cảnh mô hình (gọi tắt là MCP) là phương pháp hay nhất trong ngành để gọi hàm phía máy chủ.
Nhưng đối với tính năng gọi công cụ trên thiết bị, chúng tôi chính thức ra mắt Android MCP, khung gọi hàm hợp nhất trên Android.
Android MCP được hỗ trợ bởi thư viện AppFunctions của Jetpack. Thư viện này phù hợp chặt chẽ với MCP cả về mặt kỹ thuật lẫn khái niệm, đồng thời hỗ trợ cách triển khai Kotlin quen thuộc và tự nhiên mà nhiều nhà phát triển Android đã quen thuộc.
Điều này giúp việc triển khai API tương đối trực quan và dễ sử dụng, đồng thời bạn có thể tận dụng mọi kiến thức có được từ một quy trình tích hợp MCP phía máy chủ đã được thiết lập.
Nhiều dữ liệu gốc của MCP liên kết một với một với một cấu trúc AppFunctions của Jetpack.
Hãy cùng xem một số ví dụ minh hoạ cho điều này.
Điểm cuối của công cụ MCP liên kết chính xác với một hàm Kotlin được chú thích bằng AppFunctionDeclaration.
Các mô hình pydantic của bạn liên kết với các lớp dữ liệu Kotlin được chú thích bằng AppFunctionSerializable.
Tuy nhiên, thay vì truyền tải qua HTTP có thể truyền trực tuyến và/hoặc I/O tiêu chuẩn, MCP của Android chạy trên IPC Android Binder được tối ưu hoá.
Hôm nay, chúng tôi rất vui mừng được thông báo rằng thư viện Jetpack, AppFunctions, hiện đang ở giai đoạn thử nghiệm beta.
Được rồi, tôi chắc rằng bạn có thể đang thắc mắc khi nào nên sử dụng một đường dẫn API và khi nào nên sử dụng Android MCP.
Về nguyên tắc chung, Android MCP đặc biệt hữu ích khi bạn muốn tận dụng bối cảnh trên thiết bị hoặc bối cảnh địa phương để hoàn thành hành trình của người dùng.
Hãy cùng tìm hiểu một số ví dụ cho thấy Android MCP thực sự toả sáng.
Cụ thể, bạn nên cân nhắc sử dụng MCP Android khi cần truy cập vào dữ liệu cục bộ, đã mã hoá.
Điều này đặc biệt quan trọng đối với các ứng dụng nhắn tin và liên lạc, trong đó nhật ký tin nhắn cần được lưu giữ trên thiết bị do có tính năng mã hoá hai đầu.
Android MCP cũng hữu ích cho các hành động hoàn toàn trên thiết bị và khi không có mạng, trong trường hợp tác nhân có hỗ trợ.
Đối tác Wear OS của chúng tôi, Samsung Health, đã ra mắt một chức năng để bắt đầu chạy và bắt đầu bài tập thể dục, được điều phối hoàn toàn trên thiết bị.
Người dùng thường muốn các loại hành động này được thực hiện rất nhanh, có độ tin cậy cao, độ trễ thấp và đôi khi, ngay cả trong môi trường có kết nối kém, việc đảm bảo rằng toàn bộ quá trình gọi công cụ và định tuyến cũng như định tuyến truy vấn có thể diễn ra trên thiết bị sẽ đảm bảo điều này.
Android MCP cũng là đường dẫn API ưu tiên nếu bạn muốn truy cập vào cảm biến của thiết bị hoặc tương tác trực tiếp với phần cứng của thiết bị.
Ví dụ: nhiều ứng dụng ô tô muốn cho phép người dùng mở khoá cửa xe ngay lập tức thông qua kết nối UWB và thường rảnh tay.
Vậy thì Android MCP có thể giúp bạn.
Cuối cùng, đây là giải pháp phù hợp nếu không có Cloud Endpoints nào cho ứng dụng hoặc API của bạn.
Nhiều ứng dụng bên thứ nhất của nhà sản xuất thiết bị chỉ hoạt động ở phía máy khách, nên họ sử dụng MCP Android vì lý do này.
Ví dụ: Bạn có thể dùng Gemini để tìm và truy xuất ảnh từ các ứng dụng thư viện ảnh chỉ hoạt động ở phía máy khách, chẳng hạn như Xiaomi Gallery.
Tại đây, tôi có thể mở Gemini và yêu cầu: "Tìm ảnh robot Android mà tôi đã chụp tuần trước trong Xiaomi Gallery".
Xiaomi đã cung cấp một chức năng của ứng dụng để tìm ảnh. Sau đó, Gemini sẽ gọi chức năng này và các ảnh liên quan sẽ được trả về và hiển thị ngay trong giao diện người dùng của ứng dụng Gemini, nhờ đó người dùng không cần phải rời khỏi ứng dụng.
Chúng tôi cũng dựa vào chính cấu trúc đó để thực hiện các hành động trên thiết bị trong hệ điều hành Android.
Android MCP hỗ trợ tính năng Trợ giúp thiết bị mới ra mắt gần đây, một tính năng tích hợp trực tiếp hệ điều hành Android vào ứng dụng Gemini.
Tính năng này đã ra mắt trên các thiết bị Pixel và Lenovo, đồng thời đang được mở rộng trên hệ sinh thái OEM Android rộng lớn hơn.
Người dùng có thể sử dụng tính năng Trợ giúp thiết bị bằng cách truy cập vào Gemini và hỏi cách tối ưu hoá các chế độ cài đặt, chế độ cài đặt thiết bị và chế độ cài đặt hệ thống để cải thiện trải nghiệm khi dùng thiết bị.
Ví dụ: thay vì tìm trong nhiều trình đơn, một người dùng bị mù màu có thể hỏi Gemini: "Tôi bị mù màu.
Làm cách nào để điện thoại này hoạt động hiệu quả hơn cho tôi?"
Gemini sẽ gọi và kích hoạt các chức năng của ứng dụng cục bộ, đồng thời suy luận một cách thông minh thông qua yêu cầu và đề xuất những cách thực tế mà người dùng có thể thay đổi chế độ cài đặt hệ thống, cũng như những cách cụ thể mà họ có thể thay đổi chế độ cài đặt thiết bị để cải thiện trải nghiệm.
Sau đó, các chế độ kiểm soát trong ứng dụng sẽ xuất hiện ngay trên giao diện người dùng của ứng dụng Gemini để người dùng có thể kiểm soát ngay lập tức.
Ví dụ: để đáp ứng yêu cầu tối ưu hoá cho người mù màu, Gemini đã hiển thị nút bật/tắt tính năng chỉnh màu này để người dùng có thể thay đổi ngay lập tức.
Trải nghiệm MCP trên Android đã có trên một số thiết bị và chúng tôi đang hợp tác với nhiều đối tác để mang đến nhiều trường hợp sử dụng hơn nữa cho hệ sinh thái Android.
Mặc dù việc khai báo rõ ràng công cụ MCP của Android là tiêu chuẩn vàng của chúng tôi, nhưng chúng tôi biết rằng sẽ có những trường hợp người dùng yêu cầu tác nhân hoàn thành các trường hợp sử dụng phức tạp hoặc ít phổ biến hơn. Đây chính là lúc tính năng Tự động hoá tác vụ dựa trên nền tảng của chúng tôi phát huy tác dụng.
Theo mô hình này, tác nhân được tải sẵn của OEM có thể sử dụng API điều khiển máy tính của Android để mô phỏng các sự kiện chạm trên giao diện ứng dụng.
Nhà phát triển không cần thay đổi mã và tác nhân cần nhấn vào giao diện người dùng như người dùng bằng cách phân tích màn hình ứng dụng.
Để đảm bảo quyền kiểm soát và sự an toàn của người dùng, tác nhân cần phải tạm dừng khi thực hiện các hành động có thể nhạy cảm (chẳng hạn như quy trình thanh toán) và yêu cầu người dùng xác nhận để tiếp tục.
Hãy cùng xem một ví dụ thực tế.
Trong trường hợp này, người dùng đã chụp ảnh một chuyến tham quan cà phê và sô cô la, và họ muốn hỏi Gemini hoặc muốn yêu cầu Gemini tìm hiểu trên Expedia xem có thể tìm thấy một chuyến tham quan tương tự hay không, nhưng điều chỉnh số người tham dự thành 6 người.
Không cần tích hợp API trực tiếp, Gemini vẫn có thể điều hướng giao diện người dùng của Expedia ở chế độ nền giống như cách người dùng thực hiện.
Sau đó, Gemini sẽ hiển thị các lựa chọn phù hợp mà Gemini tìm được và chuyển giao liền mạch cho người dùng khi đến lúc xác nhận và đặt chỗ lần cuối.
Bạn cần hiểu rằng ba con đường này không loại trừ lẫn nhau mà thực ra còn bổ trợ cho nhau.
Người dùng và nhà phát triển nên mong đợi các tác nhân có thể điều phối các công cụ trên các bản triển khai máy chủ và sau đó là ứng dụng Android cục bộ để mang lại trải nghiệm toàn diện và đầy đủ nhất cho người dùng.
Được rồi.
Cảm ơn Sarah.
Chúng ta sẽ tìm hiểu sâu hơn một chút về MCP Android.
Và khi có thể, tôi muốn bạn biết rằng tôi đang lấy các đoạn mã MCP từ Jetpacker, ứng dụng mẫu về du lịch của chúng tôi, thực chất được xây dựng với sự trợ giúp của Antigravity.
Bạn đã thấy điều này trong bài phát biểu chính.
Ứng dụng này có đủ loại tính năng thông minh thú vị, bao gồm cả các tính năng tích hợp trên thiết bị với API AI tạo sinh của Bộ công cụ học máy và API nhận dạng lời nói của Bộ công cụ học máy.
Giờ đây, MCP Android và MCP dựa trên đám mây có cấu trúc tương tự nhau.
Cả hai đều có máy khách và máy chủ.
Giờ đây, trong thế giới MCP, máy chủ cung cấp các công cụ, còn máy khách có thể khám phá và gọi các công cụ này.
Giờ đây, với Android MCP, ứng dụng của bạn là máy chủ và tác nhân là ứng dụng khách.
Nền tảng Android đóng vai trò là sổ đăng ký công cụ.
Giờ đây, các ứng dụng bắt đầu bằng cách đăng ký một dịch vụ để cung cấp máy chủ và chúng ta sẽ bắt đầu bằng cách thêm các phần phụ thuộc thư viện AppFunctions Jetpack vào mẫu của chúng ta tại đây.
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)Sau đó, chúng ta sẽ sử dụng chú thích KSP để tạo AppFunctionService.
Giờ đây, dịch vụ này giúp chức năng của ứng dụng có thể thực sự tuân thủ đúng vòng đời của Android.
Chúng ta có thể sử dụng tính năng chèn phần phụ thuộc bằng Hilt để chèn các kho lưu trữ dữ liệu mà AppFunctions có thể sử dụng.
@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
...Rất tiếc.
Quay lại sơ đồ của chúng ta.
Sau khi thiết lập dịch vụ cho máy chủ MCP trên Android, ứng dụng sẽ đăng ký các công cụ dưới dạng AppFunctions, cùng với mọi mô hình dữ liệu liên quan mà các công cụ đó sử dụng.
Giờ đây, bạn có thể đăng ký một mô hình dữ liệu bằng chú thích AppFunctionSerializable và KDoc này khá thú vị.
/** 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,
)Nó sẽ chỉ trở thành siêu dữ liệu được tác nhân sử dụng để hiểu cách sử dụng mô hình.
Bạn có thể sử dụng chú thích AppFunctionDeclaration để đăng ký hàm của mình dưới dạng một công cụ.
@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) }
}
}Khi isDescribedByKDoc được đặt thành true, KDoc sẽ cung cấp cho tác nhân một nội dung mô tả để hiểu chức năng của hàm, đồng thời giúp con người hiểu được hàm đó.
/**
* 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()
...Sau đây là một số phương pháp hay nhất để thiết kế tham số.
/**
* @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)Trước hết, bạn nên tập trung vào một nhóm nhỏ các tham số có tên rõ ràng, riêng biệt.
Đặt các tham số không cần thiết thành có thể rỗng và phân giải giá trị mặc định của các tham số đó trong phần nội dung hàm.
Đây không chỉ là một phương pháp hay nhất.
Cách này thực sự không hiệu quả vì các giá trị mặc định của Kotlin sẽ bị bỏ qua do thư viện AppFunctions truyền giá trị rỗng cho các tham số có thể rỗng bị bỏ qua.
Tôi thậm chí sẽ không đưa ra ví dụ về trường hợp đầu tiên này, nhưng đừng sử dụng tham số chung chung.
Những thứ như bối cảnh đầy đủ hoặc nhật ký trò chuyện, chúng chỉ là những thứ tồi tệ.
Chúng có thể khiến các tác nhân truyền một lượng lớn dữ liệu và dẫn đến việc rò rỉ dữ liệu, và đây là một phương pháp không tốt.
Vì AppFunction thực thi theo mặc định trên luồng giao diện người dùng, nên bạn cần đảm bảo mọi thao tác chạy trong thời gian dài đều diễn ra ở chế độ nền bằng cách chọn trình điều phối coroutine thích hợp.
@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 }
...Bạn nên gửi ngoại lệ AppFunction khi cần thiết kèm theo thông tin chi tiết để tác nhân có thể hiểu cách giải quyết lỗi.
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
message: Message
): Result {
...
throw AppFunctionException(
errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
errorMessage = "To address has invalid syntax!"
)
}Được rồi, giờ chúng ta đã có một AppFunction, hãy quay lại sơ đồ cấu trúc ở đây.
Ứng dụng khách của tác nhân đăng ký với nền tảng để gọi AppFunctions.
Ví dụ: Gemini là một mô hình như vậy.
Để làm được việc này, các tác nhân cần có quyền đặc quyền và sau đó, các tác nhân đã đăng ký có thể tra cứu siêu dữ liệu AppFunction từ Nền tảng Android và quyết định xem có nên thực thi một hoặc nhiều AppFunction đã đăng ký để thực hiện yêu cầu của người dùng hay không.
Giờ thì, nói về một hoặc nhiều AppFunction, tác nhân thực sự quyết định như thế nào?
Một việc cuối cùng bạn cần làm là mô tả các tính năng của ứng dụng trong tệp siêu dữ liệu của ứng dụng.
<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" />Và tác nhân chọn công cụ bằng cách trước tiên đọc nội dung mô tả này trong AppFunctionAppMetadata, sau đó đọc nội dung mô tả của từng AppFunction.
Vì vậy, bạn nên tập trung vào phần đầu của nội dung mô tả bằng cách giải thích ngắn gọn về lĩnh vực chính của ứng dụng hoặc công cụ.
Sau đó, bạn có thể cung cấp các mẫu vận hành để giúp tác nhân hiểu được quy trình logic của các thao tác. Điều này rất hữu ích cho việc vạch ra các điều kiện tiên quyết hoặc hướng dẫn tác nhân đi theo lộ trình tối ưu để hoàn thành nhiệm vụ.
Bạn cũng có thể thêm một phần về các quy tắc ràng buộc để xác định giới hạn logic của các kiểu dữ liệu đầu vào hoặc củng cố các mẫu hoạt động.
Đó là những thông tin cơ bản về MCP trên Android, nhưng bạn có thể làm thêm một số việc khác.
Một điều thú vị mà bạn có thể làm là bật và tắt AppFunctions một cách linh động.
viewModelScope.launch {
val newState =
if (enabled) {
AppFunctionManager.APP_FUNCTION_STATE_ENABLED
} else {
AppFunctionManager.APP_FUNCTION_STATE_DISABLED
}
appFunctionManager?.setAppFunctionEnabled(id, newState)
}Quan trọng: Ví dụ: nếu bạn muốn AppFunction chỉ hoạt động khi người dùng đã đăng nhập.
Một việc khác bạn có thể làm là tạo AppFunctions trong phạm vi một màn hình hoặc hoạt động cụ thể trong ứng dụng.
@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
}Vì vậy, thay vì khai báo AppFunction, bạn sẽ khai báo AppFunctionSignature.
Sau đó, bạn có thể triển khai trình xử lý cho AppFunction trong giao diện người dùng.
val addToCartAppFunctionAdapter =
appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()
coroutineScope.launch {
appFunctionManager.handleAppFunction(
addToCartAppFunctionAdapter.adapt { quantity ->
val newCartSize = cart.addItem(quantity)
newCartSize
}
)
}Và điều thú vị là vòng đời của trình xử lý sẽ tự động gắn liền với vòng đời của phạm vi coroutine mà bạn sử dụng.
Bây giờ, bạn sẽ muốn lưu trình chuyển đổi yêu cầu AppFunctions này vào bộ nhớ đệm, xin lỗi, trình chuyển đổi yêu cầu AppFunctions xử lý, vì thư viện sử dụng tính năng phản chiếu nâng cao để tạo thực thể cho trình chuyển đổi này.
Ngoài ra, còn có chức năng tạo AppFunction trong phạm vi mà việc đăng ký không liên kết trực tiếp với vòng đời của Android.
Bây giờ, điều cuối cùng là chúng ta có một thứ gọi là cấp truy cập.
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
return withContext(Dispatchers.IO) {
...Bạn có thể nhận và sử dụng cấp truy cập của chính bạn, của hệ thống hoặc của tác nhân đáng tin cậy. Cấp truy cập này cho biết những hàm nào chỉ dành cho tác nhân trong ứng dụng của bạn (chẳng hạn như tác nhân được xây dựng bằng Bộ công cụ học máy GenAI), những hàm nào chỉ dành cho hệ thống và những hàm nào dành cho các tác nhân hệ thống đáng tin cậy.
Để giúp bạn xây dựng tác nhân trong ứng dụng, chúng tôi đã thông báo rằng Bộ công cụ học máy hỗ trợ sẵn AppFunctions trong khung gọi công cụ hợp nhất.
Bạn chỉ cần xác định các công cụ này một lần và có thể sử dụng cho cả tác nhân cấp hệ thống và mô hình cục bộ trong ứng dụng, giúp bạn tránh được tình trạng khai báo công cụ rời rạc.
Để tìm hiểu thêm, bạn có thể xem bài nói chuyện này, Khai thác khả năng của tác nhân AI trong ứng dụng bằng Bộ công cụ học máy GenAI, hoặc bạn có thể truy cập và xem những nội dung mới nhất trong Jetpack.
Thật thú vị.
Được rồi, giờ hãy quay lại với Sarah.
Được rồi.
Dan vừa cho bạn thấy cách hoạt động của MCP Android.
Hãy áp dụng vào thực tế.
Hãy nhớ rằng ứng dụng của bạn là máy chủ và tác nhân là ứng dụng khách, vì vậy, mục tiêu là đảm bảo ứng dụng của bạn là một máy chủ tốt cho các tác nhân đó.
Chúng ta sẽ theo dõi Jetpacker, ứng dụng mẫu về du lịch, từ lần khai báo công cụ đầu tiên đến lệnh gọi hàm thực tế thông qua Gemini.
Bạn có thể tự viết AppFunctions hoặc sử dụng tác nhân lập trình để bắt đầu.
Chúng tôi đã phát hành kỹ năng AppFunctions có thể giúp bạn bắt đầu, bắt đầu viết các hàm mà bạn có thể kết nối với Android Studio thông qua Android CLI hoặc sử dụng với tác nhân lập trình mà bạn chọn.
android skills add appfunctionsKỹ năng này hoạt động theo 3 bước.
Trước tiên, Gemini sẽ phân tích toàn bộ mã nguồn của bạn để xác định những hành trình chính của người dùng có thể tạo ra các công cụ hữu ích, chẳng hạn như phát nội dung nghe nhìn, tạo ghi chú hoặc gửi tin nhắn.
Sau đó, thư viện này sẽ tạo ra các khai báo AppFunction để hiển thị những hành trình đó, hoàn chỉnh với việc triển khai và hỗ trợ nền tảng mà chúng cần.
Ví dụ: Gradle và KSP, XML siêu dữ liệu ứng dụng và chèn phần phụ thuộc.
Cuối cùng, Gemini sẽ tinh chỉnh nội dung mô tả KDoc vì đây chính là thông tin mà tác nhân sử dụng để quyết định thời điểm và cách gọi hàm của bạn.
Đây là loại kết quả mà chúng ta đang tìm kiếm.
Đây là hàm addExpense cho 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))
}Nội dung mô tả là một câu gồm một dòng, bắt đầu bằng một động từ và một tài nguyên: thêm chi phí vào chuyến đi.
Các trường chuyến đi và danh mục có thể rỗng vì người dùng không phải lúc nào cũng chỉ định các trường này, nhưng hàm sẽ phân giải các trường này trong phần nội dung.
Và khi một yêu cầu không thành công, yêu cầu đó sẽ gửi một AppFunctionException để tác nhân biết được vấn đề.
Giờ hãy cùng tìm hiểu về việc thử nghiệm.
Giờ đây, các hàm của bạn là các dịch vụ mà nhân viên hỗ trợ có thể sử dụng.
Vì vậy, bạn cần kiểm thử hai phần: cách hàm hoạt động và cách tác nhân hoạt động.
Đối với hàm của bạn, chúng ta sẽ bắt đầu bằng kiểm thử đơn vị mà có thể bạn đã viết, nhưng có một số điều cần lưu ý khi tác nhân là bên thực sự thực hiện lệnh gọi hàm.
Thứ nhất, các giá trị mặc định của Kotlin không áp dụng ở đây.
Tham số bị bỏ qua sẽ có giá trị rỗng, false hoặc 0.
Vì vậy, hạn mức trước đây mặc định là 10 sẽ thực sự là 0 ở đây.
Lệnh gọi cũng bắt đầu trên luồng chính và việc gọi hàm từ một kiểm thử có thể không phải lúc nào cũng rõ ràng, vì vậy, bạn nên chạy IO tiêu chuẩn trên một trình điều phối nền.
Khi có lỗi xảy ra, hãy gửi AppFunctionException.
Mọi lỗi khác sẽ được gửi đến nhân viên hỗ trợ dưới dạng lỗi chung không xác định kèm theo thông báo ngoại lệ thô.
Và dữ liệu đầu vào của bạn sẽ đến từ một mô hình, vì vậy, bạn chắc chắn nên kiểm thử các trường hợp hiếm gặp như giá trị trống, từ đồng nghĩa với đô la như USD và giá trị nằm ngoài phạm vi.
Về phía tác nhân, bạn sẽ muốn biết liệu tác nhân có chọn công cụ của bạn vào đúng thời điểm hay không và liệu tác nhân có thực thi hàm đúng cách hay không.
Mỗi tác nhân hoạt động theo một cách khác nhau, vì vậy, bạn cần kiểm thử các hàm của mình với bất kỳ tác nhân nào mà bạn dự kiến sẽ sử dụng các hàm đó.
Bạn có thể dùng tác nhân mẫu nguồn mở của chúng tôi để kiểm thử hoặc bạn có thể dùng Gemini để kiểm thử quy trình tích hợp này.
Nếu muốn xem cách Gemini tương tác với AppFunctions hoặc muốn chạy Android MCP trên ứng dụng Gemini, bạn có thể thực hiện việc này bằng Nền tảng ứng dụng được kết nối mà chúng tôi đã ra mắt trước đó và hiện đang ở giai đoạn xem trước.
Bạn có thể tìm hiểu thêm bằng cách xem bài nói chuyện trước đây của chúng tôi về cách mở rộng ứng dụng sang Gemini và Tìm kiếm thông qua Connected Apps Platform.
Sau khi tham gia chương trình xem trước, bạn chỉ cần thực hiện hai bước để thử nghiệm với Gemini.
Trước tiên, trong Google Connected Console, chúng ta sẽ tạo một ứng dụng được kết nối mà Gemini có thể sử dụng.
Thứ hai, trong Play Console, chúng ta cần tải gói ứng dụng lên để có thể truy cập vào AppFunctions và bạn có thể bắt đầu trên kênh thử nghiệm nội bộ để người kiểm thử có thể xác thực các hàm.
Vậy thì hãy bắt đầu trong Google Connected Console.
Tại đây, bạn sẽ tạo một ứng dụng được kết nối bằng nút "Tạo ứng dụng" ở trên cùng bên phải.
Khi bạn tạo ứng dụng đã kết nối, hãy chọn Android MCP trên màn hình này.
Nếu đang xây dựng một bản triển khai MCP phía máy chủ, thì đây là nơi bạn sẽ tải mã máy chủ lên.
Khi bạn nhấp vào "Thiết lập", mọi ứng dụng trên Google Play được liên kết với tài khoản nhà phát triển của bạn sẽ xuất hiện tại đây để bạn liên kết với ứng dụng đã kết nối.
Nếu chưa có ứng dụng trên Google Play, trước tiên, bạn cần tạo một ứng dụng trong Play Console.
Ở đây, chúng ta đã có Jetpacker, vì vậy, chúng ta sẽ tiếp tục và liên kết Jetpacker.
Tiếp theo, hãy thêm thông tin chi tiết mà người dùng sẽ thấy trong giao diện người dùng của Gemini khi họ cố gắng tương tác với ứng dụng của bạn.
Thông tin này bao gồm những nội dung như biểu tượng ứng dụng, tên hiển thị, các chức năng của ứng dụng có thể kết nối và các câu lệnh mẫu mà người dùng có thể sử dụng để tương tác với ứng dụng.
Những nội dung này phải mang tính mô tả để hướng dẫn người dùng một cách hiệu quả nhất.
Đến đây là hoàn tất quá trình thiết lập ứng dụng có thể kết nối, nhưng có thể bạn đang thắc mắc làm thế nào để ứng dụng có thể kết nối thực sự có quyền truy cập vào các chức năng.
Vì các hàm này nằm trong mã ứng dụng, nên giờ đây, chúng ta cần truy cập vào Play Console để tải gói ứng dụng lên và tạo một kênh thử nghiệm, nhờ đó có thể thử nghiệm với người dùng thực tế.
Vì vậy, trên màn hình này, chúng ta sẽ tải gói ứng dụng lên, sau đó chúng ta sẽ thấy Play có thể phát hiện AppFunctions và bạn có thể liên kết trực tiếp các hàm đó với ứng dụng có thể kết nối của mình.
Giờ đây, chúng ta có thể thấy ứng dụng được kết nối với Jetpack đã hoàn tất và tích hợp các chức năng của bạn.
Tiếp theo, chúng ta cần thêm người kiểm thử để có người thực sự sẵn sàng xác thực các hàm và đảm bảo các hàm đó hoạt động đúng cách.
Vì vậy, hãy nhấp vào phiên bản ứng dụng đã kết nối tại đây.
Trên màn hình này, bạn có thể chọn "Quản lý người thử nghiệm" và thiết lập một danh sách giới hạn gồm những người thử nghiệm có thể truy cập vào ứng dụng có thể kết nối của bạn trong Gemini sau khi ứng dụng đó được cung cấp.
Bạn có thể thêm người kiểm thử vào kênh kiểm thử này giống như cách bạn làm cho mọi bản phát hành khác.
Đảm bảo rằng bạn có cùng danh sách người thử nghiệm cho ứng dụng được kết nối và ứng dụng trên Google Play.
Bây giờ, hãy thử nghiệm quá trình tích hợp.
Đây là ứng dụng Jetpacker trên điện thoại của người dùng, đã ghi lại một chuyến đi cho hội nghị thượng đỉnh này.
Tôi sẽ mở Gemini để kết nối ứng dụng.
Tôi cần đảm bảo rằng phiên bản ứng dụng cài đặt trên thiết bị của tôi giống với phiên bản chúng tôi đã tải lên Play trước đó, và chúng tôi nên sử dụng một tài khoản cho Gemini đã được thêm vào danh sách người kiểm thử.
Vì vậy, ở đây, tôi sẽ đề cập đến Jetpacker và yêu cầu mã xác nhận phòng khách sạn của tôi ở Mountain View.
Gemini gọi AppFunctions của Jetpacker trên thiết bị và trả lời: "RED11223 tại Khách sạn Redwood Grove".
Tiếp theo, tôi sẽ hỏi: "Tôi còn bao nhiêu ngân sách cho Google Ads?"
Và lệnh gọi hàm sẽ trả về 1.850 USD.
Bây giờ, tôi muốn thay đổi nên sẽ yêu cầu Gemini thêm 45 USD cho chuyến đi Uber đến sân bay vào chuyến đi ADS của tôi.
Gemini gọi hàm addExpense mà chúng ta đã tạo trước đó để thêm trực tiếp thông tin này vào hành trình của tôi.
Và khi tôi mở Jetpacker, khoản chi phí đã có sẵn ở đó và ngân sách cũng giảm tương ứng.
Nếu bạn muốn bắt đầu xây dựng ứng dụng kết nối cho Gemini ngay hôm nay, hãy đăng ký quyền truy cập vào Connected Apps Platform (hiện đang ở giai đoạn xem trước).
Được rồi.
Hãy cùng tổng kết.
Vì vậy, chúng ta đang ở giai đoạn đầu của một sự thay đổi lớn trong lĩnh vực điện toán di động và đã đến lúc bạn cần đảm bảo rằng ứng dụng của mình đã sẵn sàng.
Vì vậy, hãy bắt đầu bằng cách xác định hành trình cốt lõi của người dùng có giá trị cao trong ứng dụng.
Hiển thị phần phụ trợ của đám mây bằng MCP phía máy chủ và sử dụng MCP Android để làm việc với dữ liệu được mã hoá hai đầu, hoạt động cục bộ, cảm biến thiết bị và các thành phần khác trên thiết bị.
Giờ đây, để tạo nguyên mẫu, bạn có thể tải Android Studio và Android CLI xuống.
Bạn có thể thêm phần phụ thuộc AndroidX AppFunctions beta vào tệp bản dựng và kiểm thử chức năng đầu tiên trên thiết bị tại máy của bạn.
Để đăng ký sử dụng sớm Gemini trong môi trường thực tế, bạn có thể đăng ký thông qua mã QR này.
Và tất nhiên, hãy nhớ xem các bài nói chuyện liên quan về cách làm việc với Connected Apps Platform, cũng như cách bạn có thể sử dụng AppFunctions nội bộ với Bộ công cụ học máy hoặc với tác nhân của riêng bạn để điều khiển tác nhân trong ứng dụng của riêng bạn.
Cuối cùng, cảm ơn bạn rất nhiều vì đã dành thời gian.
Chúng tôi rất mong được thấy tất cả những trải nghiệm tuyệt vời mà bạn đang xây dựng cho 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))
}