Умные приложения

Интегрируйте приложение с интеллектуальной системой Android

Колонки

О приложении

О выступлении

Android превращается в интеллектуальную систему, в которой агенты, такие как Gemini, координируют задачи в разных приложениях. Чтобы открыть новые возможности для пользователей и участвовать в этих рабочих процессах, ваше приложение должно предоставлять доступ к своим основным функциям.

Это занятие – практическое руководство по интеграции приложения с интеллектуальным уровнем Android. Мы рассмотрим основные пути интеграции, включая MCP на устройстве Android, MCP на стороне сервера и автоматизацию задач, и предоставим схему принятия решений для выбора правильного пути для вашего варианта использования. Мы также рассмотрим, как персональный контекст помогает предугадывать потребности пользователя.

Вы научитесь:

  • Выберите оптимальный способ интеграции для вашего приложения.
  • Внедрите как реактивные, так и проактивные интеллектуальные функции.
  • Баланс между производительностью и безопасностью при интеграции агентов.

Сводка

  • Android MCP – это унифицированная платформа для вызова инструментов на устройстве, работающая на основе библиотеки Jetpack AppFunctions. Он сопоставляет примитивы MCP с конструкциями Kotlin и работает на Android Binder IPC.
  • Инструменты на устройстве отлично подходят для доступа к зашифрованным данным или датчикам устройства. Другой вариант – автоматизация задач на уровне платформы. В этом случае предустановленные агенты имитируют события касания без изменения кода.
  • Ваше приложение действует как MCP-сервер Android, а агенты обнаруживают и вызывают инструменты. Объявляйте инструменты с помощью AppFunctionDeclaration, добавляйте описания KDoc и не полагайтесь на значения по умолчанию в Kotlin.
  • Платформа подключенных приложений, которая сейчас находится на этапе предварительного доступа, позволяет тестировать AppFunctions с Gemini. Настройте связанное приложение в Google Connected Console, а затем загрузите набор App Bundle в Play Console.

Ресурсы

Gemini Notebook

Темы

  • android-mcp
  • appfunctions
  • gemini
  • connected-apps
  • model-context-protocol
  • on-device-ai

Расшифровка

Введение в эру искусственного интеллекта

  • Теперь

  • Здравствуйте!

  • Меня зовут Дэн Галпин, я работаю в отделе по связям с разработчиками.

  • А я Сара Шамси, менеджер по продукту, работающий над фреймворками для разработчиков Android.

  • Мы рады приветствовать вас на сегодняшнем занятии.

  • Мы наблюдаем смену парадигмы взаимодействия пользователей с устройствами.

  • В эпоху интернета пользователи обычно начинали свой путь с открытия браузера и использования функции поиска.

  • Затем, в эпоху мобильных устройств, поведение пользователей изменилось: они стали использовать более специализированные нативные приложения для выполнения разовых действий.

  • Например:

  • Приходит сообщение от друга с приглашением на ужин.

  • Чтобы ответить, мне нужно проверить календарь, добавить мероприятие, заказать такси, если нужно, и написать другу, что я приду.

  • Раньше для этого требовалось переключаться между приложениями на устройстве, чтобы выполнить действия и завершить путь.

  • Но сейчас мы живем в эпоху искусственного интеллекта.

  • В эпоху искусственного интеллекта пользователи начинают свой путь с выражения общих намерений и целей, а не конкретных действий, которые им необходимо выполнить.

  • Происходит фундаментальный сдвиг: пользователи начинают воспринимать свои устройства не просто как инструмент для выполнения действий, а как помощника, который предугадывает их потребности и помогает достигать целей.

  • И мы заметили, что они ожидают, что эта интеллектуальная структура будет работать не только на их мобильных устройствах, но и в богатой экосистеме форм-факторов, включая часы, автомобили, телевизоры, ноутбуки, а также XR и очки.

  • В эпоху искусственного интеллекта один запрос, охватывающий несколько приложений, может помочь вам пригласить друзей на ужин.

  • С помощью всего одного запроса агент на моем устройстве может помочь выполнить все необходимые действия: проверить календарь, добавить мероприятие, заказать поездку и отправить сообщение.

  • Сегодня мы расскажем о планах Android в этой области и о том, как вы, разработчики, можете создавать первоклассные продукты.

  • Дэн.

  • Спасибо, Сара.

  • Android продолжает добавлять интеллектуальные функции в свое ядро, и это предлагает новые способы для ваших приложений, чтобы подготовиться к этой эре интеллекта.

  • Первый из этих двух столпов – это искусственный интеллект в приложении. Он позволяет сделать ваши приложения умнее. Вы можете использовать модели на устройстве, такие как Gemini Nano 4, через ML Kit, чтобы обобщать цепочки сообщений, переписывать ответы, понимать изображения или использовать естественный язык для вызова функций вашего приложения.

  • Вы даже можете использовать облачные модели Gemini прямо в своем приложении с помощью Firebase AI Logic.

  • Но сегодня мы сосредоточимся на втором столпе – обеспечении доступности возможностей вашего приложения в интеллектуальной системе.

  • Итак, что такое интеллектуальная система?

  • Поэтому, помимо того, что вы можете говорить с ним и использовать его для работы с подсказками, о которых ранее говорила Сара, это система, которая позволяет глубоко интегрировать его, чтобы улучшить работу всей экосистемы приложений.

  • Например, устройства, разработанные для Gemini Intelligence, используют эти точки интеграции, чтобы предоставлять такие функции, как подсказки, сводки уведомлений, улучшенный голосовой ввод и автоматическое заполнение форм.

  • Теперь подсказки – это часть функции "Проактивная помощь".

  • До сих пор мы говорили о помощи по запросу.

  • Мы все к этому привыкли, когда пользователь говорит агенту выполнить одну или несколько задач от его имени.

  • Но проактивная помощь на самом деле заключается в том, чтобы предвидеть следующее действие или действия, которые может предпринять пользователь.

  • Вернемся к примеру с ужином. Агент может предложить пользователю добавить приглашение в календарь или заказать такси.

  • Но как именно он запрашивает пользователя?

  • Проактивная помощь может предлагать действия в разных местах, например на заблокированном экране, в уведомлениях, на виртуальной клавиатуре и даже в некоторых приложениях.

  • Как сделать так, чтобы ваше приложение появлялось в этих чипах и привлекало трафик?

  • Вы уже используете последнюю версию SDK и наши самые современные инструменты. Теперь лучшее, что вы можете сделать, – это убедиться, что ваше приложение готово к помощи по запросу, о которой Сара расскажет далее.

Помощь по запросу и Android MCP

  • Спасибо, Дэн.

  • Итак, помощь по запросу предполагает, что пользователи напрямую обращаются к агенту и просят его запустить и скоординировать задачи, даже в разных приложениях.

  • Это позволяет автоматизировать рабочие процессы, которые раньше выполнялись вручную и требовали много времени.

  • В нашей интеллектуальной системе на Android мы в основном сосредоточены на системных агентах, таких как Gemini, которые могут использовать приложения на устройстве пользователя.

  • Есть несколько способов, которыми ваше приложение может взаимодействовать с системным агентом.

  • Это автоматизация задач на основе платформы и прямая интеграция API в системного агента.

  • Мы всегда рекомендуем по возможности использовать прямую интеграцию с API.

  • Разработчики могут сами решать, как будет выглядеть их приложение и бренд.

  • Кроме того, они быстрее и надежнее, что делает их более удобными для пользователей.

  • Для интеграции с агентом можно использовать два пути API.

  • Model Context Protocol (MCP) – это отраслевой стандарт для вызова функций на стороне сервера.

  • Для вызова инструментов на устройстве мы представляем Android MCP – унифицированную платформу вызова функций на Android.

  • Android MCP работает на основе библиотеки Jetpack AppFunctions, которая тесно связана с MCP как технически, так и концептуально, и поддерживает привычную и идиоматичную реализацию Kotlin, знакомую многим разработчикам Android.

  • Это делает реализацию API относительно интуитивно понятной и простой в использовании, и вы можете использовать любые знания, которые вы могли получить в результате уже существующей интеграции MCP на стороне сервера.

  • Многие примитивы MCP сопоставляются с конструкцией Jetpack AppFunctions один к одному.

  • Рассмотрим несколько примеров.

  • Конечная точка инструмента MCP хорошо сопоставляется с функцией Kotlin, аннотированной с помощью AppFunctionDeclaration.

  • Модели pydantic сопоставляются с классами данных Kotlin, аннотированными с помощью AppFunctionSerializable.

  • Однако вместо потокового HTTP и/или стандартного транспорта ввода-вывода Android MCP работает на оптимизированном Android Binder IPC.

  • Сегодня мы рады сообщить, что библиотека Jetpack, AppFunctions, теперь находится на стадии бета-тестирования.

Основные примеры использования Android MCP

  • Итак, у вас наверняка возник вопрос, когда лучше использовать один путь API, а когда – Android MCP.

  • В целом Android MCP особенно полезен, когда вы хотите использовать локальный контекст или контекст на устройстве, чтобы помочь пользователю выполнить задачу.

  • Давайте рассмотрим несколько примеров, в которых Android MCP действительно сияет.

  • В частности, Android MCP следует использовать, когда вам нужен доступ к локальным зашифрованным данным.

  • Это особенно важно для приложений для обмена сообщениями и связи, где история переписки должна оставаться на устройстве из-за сквозного шифрования.

  • Android MCP также полезен для действий, выполняемых только на устройстве и в офлайн-режиме, если агенты поддерживают это.

  • Наш партнер по Wear OS, Samsung Health, запустил функцию, которая позволяет начать пробежку или тренировку, управляемую полностью на устройстве.

  • Пользователи часто хотят, чтобы такие действия выполнялись очень быстро, с высокой надежностью, низкой задержкой, а иногда и в условиях плохого подключения. Обеспечение того, чтобы весь вызов инструмента, маршрутизация и маршрутизация запросов могли происходить на устройстве, гарантирует это.

  • Android MCP также является предпочтительным API, если вы хотите получить доступ к датчикам устройства или напрямую взаимодействовать с его аппаратным обеспечением.

  • Например, многие автомобильные приложения позволяют пользователям мгновенно разблокировать двери автомобиля с помощью СШП-подключения, часто без помощи рук.

  • В этом вам поможет Android MCP.

  • Наконец, это решение подходит, если для ваших приложений или API нет доступной конечной точки в облаке.

  • Многие собственные приложения производителей устройств работают только на стороне клиента, поэтому они используют Android MCP.

  • Например, Gemini может искать и извлекать фотографии из клиентских галерей, таких как Xiaomi Gallery.

  • Я могу открыть Gemini и сказать: "Найди в Xiaomi Gallery фотографии робота Android, которые я сделал на прошлой неделе".

  • Xiaomi предоставила функцию поиска фотографий, которую вызывает Gemini. Найденные снимки показываются прямо в интерфейсе приложения Gemini, поэтому пользователю не нужно никуда переходить.

  • Та же архитектура используется для действий на устройстве в ОС Android.

  • Android MCP обеспечивает работу недавно запущенной функции "Помощь с устройством", которая представляет собой прямую интеграцию ОС Android в приложение Gemini.

  • Эта функция уже доступна на устройствах Pixel и Lenovo и сейчас внедряется в экосистеме Android.

  • Чтобы воспользоваться Помощником по устройству, нужно спросить у Gemini, как оптимизировать настройки устройства и системы, чтобы улучшить работу устройства.

  • Например, вместо того чтобы искать нужные настройки в меню, пользователь с дальтонизмом может спросить Gemini: "У меня дальтонизм.

  • Как настроить телефон, чтобы стало удобнее им пользоваться?".

  • Gemini вызовет функции локального приложения, проанализирует запрос и предложит пользователю способы изменить системные настройки и настройки устройства, чтобы улучшить работу.

  • Затем в интерфейсе приложения Gemini появляются элементы управления, позволяющие пользователям сразу же взять ситуацию под контроль.

  • Например, чтобы выполнить запрос на оптимизацию для дальтоников, Gemini показывает переключатель коррекции цвета, чтобы пользователи могли изменить настройки на лету.

  • Функции MCP для Android уже доступны на некоторых устройствах, и мы работаем с партнерами, чтобы расширить их применение в экосистеме Android.

  • Хотя явные объявления инструментов MCP для Android – это наш золотой стандарт, мы знаем, что будут случаи, когда пользователи будут просить агентов выполнить сложные или менее распространенные задачи. Именно здесь на помощь приходит наша функция автоматизации задач на основе платформы.

  • В этой модели предустановленный агент OEM может использовать API управления компьютером Android для имитации событий касания на поверхности приложения.

  • Разработчикам не нужно менять код. Агент анализирует экран приложения и нажимает на элементы интерфейса, как это делал бы пользователь.

  • Чтобы обеспечить безопасность и контроль со стороны пользователя, агенты должны приостанавливать работу перед потенциально конфиденциальными действиями, такими как платежи, и запрашивать подтверждение пользователя для продолжения.

  • Давайте рассмотрим пример.

  • В этом примере пользователь сфотографировал рекламу тура с дегустацией кофе и шоколада и хочет попросить Gemini найти похожий тур на Expedia, но для шести человек.

  • Без прямой интеграции API Gemini в фоновом режиме перемещается по интерфейсу Expedia так же, как это делал бы пользователь.

  • Затем он показывает подходящие варианты и передает управление пользователю, когда приходит время подтвердить выбор и забронировать билеты.

  • Важно понимать, что эти три пути не исключают друг друга, а дополняют.

  • Пользователи и разработчики должны ожидать, что агенты смогут координировать инструменты в разных реализациях серверов, а также локальное приложение Android, чтобы предлагать пользователям наиболее полный и целостный опыт.

Технические подробности об Android MCP

  • Теперь

  • Спасибо, Сара.

  • Давайте подробнее рассмотрим Android MCP.

  • И когда это возможно, я хочу, чтобы вы знали, что я беру фрагменты кода MCP из Jetpacker, нашего примера приложения для путешествий, которое на самом деле создано с помощью Antigravity.

  • Вы видели это в презентации.

  • В нем есть множество полезных умных функций, в том числе интеграция с API генеративного ИИ и распознавания речи ML Kit на устройстве.

  • Теперь у Android MCP и облачного MCP похожая структура.

  • В обоих случаях есть клиенты и серверы.

  • В мире MCP серверы предоставляют инструменты, а клиенты могут обнаруживать и вызывать их.

  • Теперь, благодаря Android MCP, ваше приложение выступает в роли сервера, а агент – в роли клиента.

  • Платформа Android играет роль реестра инструментов.

  • Теперь приложения начинают с регистрации сервиса для предоставления сервера, а мы начнем с добавления зависимостей библиотеки AppFunctions Jetpack в наш пример.

    implementation(libs.androidx.appfunctions)
    ksp(libs.androidx.appfunctions.compiler)
  • Затем мы используем аннотации KSP, чтобы создать AppFunctionService.

  • Теперь этот сервис означает, что функции вашего приложения могут правильно учитывать жизненные циклы Android.

  • Мы можем использовать внедрение зависимостей с помощью Hilt, чтобы внедрять хранилища данных, которые могут использовать наши AppFunctions.

    @RequiresApi(36)
    @AndroidEntryPoint
    @AppFunctionServiceEntryPoint(
        serviceName = "JetPackerAppFunctionService",
        appFunctionXmlFileName = "jetpacker_app_function_service"
    )
    abstract class BaseJetPackerAppFunctionService : AppFunctionService() {
        @Inject internal lateinit var tripDao: TripDao
        @Inject internal lateinit var expenseDao: ExpenseDao
        ...
  • Извините.

  • Вернёмся к нашей диаграмме.

  • После настройки сервиса для нашего сервера Android MCP приложение регистрирует инструменты в виде AppFunctions, а также все связанные с ними модели данных.

  • Теперь вы можете зарегистрировать модель данных с помощью аннотации AppFunctionSerializable, и этот KDoc здесь довольно крутой.

    /** Represents an event in a trip itinerary. */
    @AppFunctionSerializable(isDescribedByKDoc = true)
    data class TimelineEventSerializable(
      /** The event's unique identifier. */
      val id: String,
      /** The trip ID this event belongs to. */
      val tripId: String,
      /** The type of event (e.g., TRANSPORTATION, ACCOMMODATION, FOOD_AND_DRINK). */
      val type: String,
      /** The timestamp of the event. */
      val timestamp: Long,
      /** The title of the event. */
      val title: String,
      /** The location of the event. */
      val location: String,
      /** A description of the event. */
      val description: String?,
      /** The title of the trip this event belongs to. */
      val tripTitle: String? = null,
      /** Optional title of a matched itinerary event when added as a reminder. */
      val matchedEventTitle: String? = null,
      /** Execution status of the tool call (e.g., SUCCESS). */
      val status: String? = "SUCCESS",
      /** Human-readable completion message instructing the agent not to repeat the call. */
      val message: String? = null,
    )
  • Они станут метаданными, которые агент будет использовать, чтобы понять, как использовать модель.

  • Чтобы зарегистрировать функцию как инструмент, используйте аннотацию AppFunctionDeclaration.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
        val events =
          if (dbEvents.isNotEmpty()) { dbEvents } else {
            val fallback = DummyData.events.filter { it.tripId == targetTripId }
            if (fallback.isNotEmpty()) { eventDao.insertEvents(fallback) }
            fallback
          }
        events.map { it.toSerializable(tripTitle = trip?.title) }
      }
    }
  • Если для параметра isDescribedByKDoc задано значение true, KDoc предоставляет агенту описание, которое помогает понять, что делает функция. Это также помогает людям понять ее.

    /**
     * Retrieves the itinerary events (with their eventId, title, type, and location) for a specific
     * trip. Note: getItinerary returns event summaries only; call getEventDetails(eventId) with a
     * specific event's ID to retrieve ticket prices, hotel nightly rates, baggage allowance, or
     * visitor tips.
     *
     * @param tripId Optional ID of the trip (e.g., '2026-1'). Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        val trip = resolveTrip(tripId = tripId)
        val targetTripId = trip?.id ?: ActiveTripSession.activeTripId ?: tripId?.takeIf { it.isNotBlank() } ?: "2026-1"
        val dbEvents = eventDao.getEventsForTrip(targetTripId).first()
    ...
  • А вот несколько рекомендаций по разработке параметров.

    /**
     * @param tripId Optional ID of the trip (e.g., '2026-1').
     *   Defaults to the active trip if omitted.
     * @return A list of events in the itinerary.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
        // resolveTrip defaults to the active trip if tripId == null
        val trip = resolveTrip(tripId = tripId)
  • Прежде всего, вам нужно сосредоточиться на минимальном подмножестве параметров с четкими, понятными названиями.

  • Сделайте необязательные параметры допускающими значение NULL и разрешите их значения по умолчанию в теле функции.

  • Это не просто рекомендация.

  • Это не сработает, потому что значения по умолчанию Kotlin будут игнорироваться, так как библиотека AppFunctions передает null для опущенных параметров, допускающих значение null.

  • Я даже не буду показывать пример первого варианта, но не используйте универсальные параметры.

  • Такие вещи, как полный контекст или история разговоров, просто плохи.

  • Они могут привести к тому, что агенты будут передавать большие объемы данных, что может привести к утечке данных, и это просто плохая практика.

  • Поскольку AppFunction по умолчанию выполняется в потоке UI, вам нужно убедиться, что все продолжительные операции выполняются в фоновом режиме. Для этого выберите подходящий диспетчер сопрограмм.

    @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 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, то есть адаптер запросов handle AppFunctions, поскольку библиотека использует рефлексию для его создания.

  • Также есть возможность создавать AppFunctions с ограниченной областью действия, регистрация которых не связана напрямую с жизненным циклом Android.

  • И последнее, что нужно сделать, – это выбрать уровень доступа.

    @AppFunctionDeclaration(isDescribedByKDoc = true)
    @AppFunctionAccessLevel(
        level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
        isCompatEnforcementEnabled = true,
    )
    suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
      return withContext(Dispatchers.IO) {
      ...
  • Вы можете получить и использовать уровень доступа self, system или trusted. Это указывает, какие функции предназначены только для использования вашим собственным агентом в приложении, например созданным с помощью ML Kit GenAI, какие функции доступны только системе, а какие – доверенным системным агентам.

  • Чтобы помочь вам создать собственного агента в приложении, мы уже объявили, что ML Kit изначально поддерживает AppFunctions как часть своей унифицированной платформы вызова инструментов.

  • Вы определяете их один раз и получаете охват как агентов на уровне системы, так и локальных моделей в приложении, что позволяет избежать фрагментированных объявлений инструментов.

  • Чтобы узнать больше, посмотрите доклад "Как добавить в приложение агентные возможности с помощью ML Kit GenAI" или просто ознакомьтесь с последними новостями о Jetpack.

  • Это очень круто.

  • Вернемся к Саре.

  • Теперь

  • Дэн только что показал вам, как работает MCP для Android.

  • Давайте применим это на практике.

  • Помните, что ваше приложение – это сервер, а агенты – клиенты. Ваша цель – сделать так, чтобы приложение было хорошим сервером для них.

  • Мы рассмотрим Jetpacker, наше пример приложения для путешествий, от первого объявления инструмента до фактического вызова функции через Gemini.

  • Вы можете написать AppFunctions вручную или использовать агент программирования.

  • Мы выпустили навык AppFunctions, который поможет вам начать писать функции. Их можно подключить к Android Studio через Android CLI или использовать с выбранным вами агентом кодирования.

    android skills add appfunctions
  • Навык работает в три этапа.

  • Сначала он анализирует базу кода, чтобы определить основные пути пользователя, которые могут быть полезны, например воспроизведение медиафайлов, создание заметки или отправка сообщения.

  • Затем он генерирует, он фактически генерирует объявления AppFunction, которые раскрывают эти пути вместе с реализацией и поддержкой платформы, которые им необходимы.

  • Например, Gradle и KSP, XML-метаданные приложения и внедрение зависимостей.

  • Наконец, агент улучшит описания KDoc, поскольку именно их он использует, чтобы решить, когда и как вызывать вашу функцию.

  • Вот какого результата мы хотим добиться.

  • Это функция addExpense для Jetpacker.

    /**
     * Adds an expense to a trip.
     *
     * @param tripId ID of the trip. If null, the active trip is used.
     * @param category Expense category. If null, it's inferred from the title.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun addExpense(
        title: String,
        amount: Double,
        currency: String,
        tripId: String? = null,
        category: String? = null,
    ): ExpenseSerializable = withContext(Dispatchers.IO) {
        if (amount <= 0.0) {
            throw AppFunctionInvalidArgumentException("amount must be greater than zero")
        }
        val trip = resolveTrip(tripId)
            ?: throw AppFunctionElementNotFoundException("No trip with ID $tripId")
        val expense = Expense(trip.id, title, amount, currency,
            category = category ?: inferExpenseCategory(title))
        expenseDao.insertExpense(expense)
        expense.toSerializable(remainingBudgetUsd(trip.id))
    }
  • Описание – это однострочное предложение, которое начинается с глагола и ресурса: добавляет расход в поездку.

  • Поля trip и category могут иметь значение null, поскольку пользователи не всегда указывают эти данные, но функция разрешит их в теле.

  • Если запрос не может быть выполнен, он выдает ошибку AppFunctionException, чтобы агент знал, что пошло не так.

Тестирование функций приложения и агентов

  • Теперь давайте поговорим о тестировании.

  • Теперь ваши функции – это сервисы, которые могут использовать агенты.

  • Вам нужно будет протестировать две вещи: поведение функции и поведение агента.

  • Для вашей функции мы начнем с модульного тестирования, которое вы, вероятно, уже написали, но есть пара вещей, о которых следует помнить, когда агент фактически выполняет вызов функции.

  • Во-первых, значения по умолчанию Kotlin здесь не применяются.

  • Пропущенный параметр возвращается как null, false или 0.

  • Таким образом, ограничение, которое ранее по умолчанию было равно 10, здесь будет равно 0.

  • Вызов также начинается в основном потоке, и вызов вашей функции из теста не всегда может сделать это очевидным, поэтому вам следует запустить стандартный ввод-вывод на фоновом диспетчере.

  • Если что-то пойдет не так, вызовите AppFunctionException.

  • Все остальные ошибки будут передаваться агенту как общие неизвестные ошибки с прикрепленным сообщением о необработанном исключении.

  • А ваши входные данные будут поступать из модели, поэтому вам обязательно следует протестировать крайние сценарии, такие как пробелы, синонимы долларов, например доллары США, и значения, выходящие за пределы диапазона.

  • Что касается агента, вам нужно знать, выбирает ли он ваш инструмент в нужное время и правильно ли выполняет функцию.

  • Каждый агент ведет себя по-разному, поэтому важно протестировать функции со всеми агентами, которые будут их использовать.

  • Для тестирования интеграции можно использовать наш пример агента с открытым исходным кодом или Gemini.

Как подключить приложение к Gemini с помощью Connected Apps Platform

  • Если вы хотите посмотреть, как Gemini взаимодействует с вашими AppFunctions, или запустить Android MCP в приложении Gemini, вы можете сделать это с помощью Connected Apps Platform, которую мы запустили ранее сегодня и которая в настоящее время находится на стадии предварительной версии.

  • Чтобы узнать больше, посмотрите наше предыдущее выступление о том, как расширить возможности приложения, подключив его к Gemini и Поиску через Connected Apps Platform.

  • После того как вы присоединитесь к программе предварительного доступа, вам нужно будет выполнить два шага, чтобы протестировать Gemini.

  • Сначала в Google Connected Console мы создадим подключенное приложение, которое сможет использовать Gemini.

  • Во-вторых, в Play Console нужно загрузить набор App Bundle, чтобы мы могли получить доступ к AppFunctions. Затем вы сможете запустить канал внутреннего тестирования, чтобы тестировщики проверили функции.

  • Начнем с Google Connected Console.

  • Нажмите кнопку "Создать приложение" в правом верхнем углу.

  • При создании подключенного приложения выберите на этом экране Android MCP.

  • Если бы вы создавали серверную реализацию MCP, то загрузили бы сюда код сервера.

  • Когда вы нажмете "Настроить", здесь появятся все приложения Google Play, связанные с вашим аккаунтом разработчика. Вы сможете связать их со своим подключенным приложением.

  • Если у вас нет приложения Google Play, сначала создайте его в Play Console.

  • В нашем случае это Jetpacker.

  • Затем добавьте информацию, которую пользователи будут видеть в интерфейсе Gemini при попытке взаимодействия с вашим приложением.

  • Это значок, отображаемое название, возможности приложения и примеры запросов, которые пользователи могут отправлять.

  • Они должны быть описательными, чтобы пользователям было проще ориентироваться.

  • На этом настройка подключенного приложения завершена. Но у вас может возникнуть вопрос, как оно получает доступ к функциям.

  • Поскольку функции содержатся в коде приложения, теперь нам нужно перейти в Play Console, загрузить набор App Bundle и создать тестовую версию, чтобы проверить работу приложения с реальными пользователями.

  • На этом экране мы загрузим набор App Bundle, а затем увидим, что Google Play может обнаружить AppFunctions, и вы сможете связать их непосредственно с подключенным приложением.

  • Теперь мы видим подключенное приложение Jetpack с интегрированными функциями.

  • Теперь нужно добавить тестировщиков, которые будут проверять работу функций.

  • Нажмите на версии подключенного приложения.

  • На этом экране вы можете выбрать "Управление тестировщиками" и настроить ограниченный список тестировщиков, которые смогут получить доступ к вашему подключенному приложению в Gemini, когда оно станет доступно.

  • Добавить тестировщиков в этот тип тестирования можно так же, как и в любой другой.

  • Убедитесь, что список тестировщиков для подключенного приложения совпадает со списком для приложения в Google Play.

Демонстрация интеграции приложения Gemini

  • Теперь давайте протестируем интеграцию.

  • Вот приложение Jetpacker на телефоне пользователя, в котором уже зарегистрирована поездка на этот саммит.

  • Я открою Gemini, чтобы подключить приложение.

  • Мне нужно убедиться, что на моем устройстве установлена та же версия приложения, которую мы ранее загрузили в Google Play, и что мы используем аккаунт Gemini, добавленный в список тестировщиков.

  • Я упомяну Jetpacker и попрошу найти номер подтверждения бронирования отеля в Маунтин-Вью.

  • Gemini вызывает AppFunctions Jetpacker на устройстве и отвечает: "RED11223 в отеле Redwood Grove".

  • Затем я спрошу: "Сколько бюджета у меня осталось на рекламу?"

  • Вызов функции вернет значение 1850.

  • Теперь я хочу внести изменения, поэтому попрошу Gemini добавить 45 долларов за Uber до аэропорта в поездку на ADS.

  • Gemini вызывает функцию addExpense, чтобы добавить это прямо в маршрут.

  • Когда я открываю Jetpacker, расход уже там, а бюджет соответственно уменьшен.

  • Если вы хотите начать создавать подключенные приложения для Gemini уже сегодня, подайте заявку на доступ к Connected Apps Platform, которая сейчас находится на этапе предварительной версии.

Заключение и будущее мобильных агентов

  • Теперь

  • Давайте подведем итоги.

  • Мы находимся в начале масштабного перехода в мобильных вычислениях, и сейчас самое время убедиться, что ваше приложение готово.

  • Для начала определите основные пути пользователей в приложении.

  • Откройте доступ к серверной части облака с помощью серверного MCP и используйте Android MCP для работы с данными, защищенными сквозным шифрованием, локальными операциями, датчиками устройства и другими функциями на устройстве.

  • Чтобы создать прототип, скачайте Android Studio и Android CLI.

  • Вы можете добавить зависимость бета-версии AndroidX AppFunctions в файлы сборки и протестировать первую функцию на устройстве локально.

  • Чтобы зарегистрироваться для раннего начала работы с Gemini в рабочей среде, вы можете отсканировать этот QR-код.

  • И, конечно, не забудьте посмотреть связанные доклады о работе с Connected Apps Platform, а также о том, как использовать внутренние AppFunctions с ML Kit или с собственным агентом, чтобы управлять собственным агентом в приложении.

  • Спасибо, что уделили нам время.

  • Нам не терпится увидеть, какие замечательные продукты вы создадите для Android.

Код

Фрагменты кода

Фрагмент скрипта Kotlin на 11:56 в видео "Технический обзор Android MCP"
implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)
Фрагмент кода Kotlin на 12:14 в видео "Технический обзор Android MCP"
@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
    ...
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
/** 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,
)
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
@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) }
  }
}
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
/**
 * 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()
...
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
/**
 * @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 в видео "Технический обзор Android MCP"
@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 }
        ...
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
@AppFunctionDeclaration(isDescribedByKDoc = true)
suspend fun send(
    message: Message
): Result {
  ...
  throw AppFunctionException(
    errorCode = AppFunctionException.ErrorCode.ERROR_INVALID_ARGUMENT,
    errorMessage = "To address has invalid syntax!"
  )
}
XML snippet at 14:15 in "Technical deep dive into Android MCP"
<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" />
Фрагмент кода на Kotlin в видео "Технические подробности о платформе Android MCP"
viewModelScope.launch {
    val newState =
        if (enabled) {
            AppFunctionManager.APP_FUNCTION_STATE_ENABLED
        } else {
            AppFunctionManager.APP_FUNCTION_STATE_DISABLED
        }
    appFunctionManager?.setAppFunctionEnabled(id, newState)
}
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
@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
}
Фрагмент кода Kotlin на 15:22 в видео "Технический обзор платформы медиаконтроллеров Android"
val addToCartAppFunctionAdapter =
    appFunctionManager.getHandleAppFunctionRequestAdapter<AddCurrentItemToCart>()

coroutineScope.launch {
    appFunctionManager.handleAppFunction(
        addToCartAppFunctionAdapter.adapt { quantity ->
            val newCartSize = cart.addItem(quantity)
            newCartSize
        }
    )
}
Фрагмент кода на Kotlin в видео "Технические подробности о платформе медиаконтента для Android"
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
    level = AppFunctionMetadata.ACCESS_LEVEL_SELF,
    isCompatEnforcementEnabled = true,
)
suspend fun getItinerary(tripId: String? = null): List<TimelineEventSerializable> {
  return withContext(Dispatchers.IO) {
  ...
Фрагмент Shell на 17:05 в видео "Technical deep dive into Android MCP"
android skills add appfunctions
Фрагмент кода на Kotlin в видео "Технический обзор Android MCP"
/**
 * 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))
}