Голосовизация приложения (beta)

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

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

  • Для корректной работы голосового управления приложение должно обязательно поддерживать спецификацию доступности (accessibility).
  • Приложения с проигрываемым контентом (музыка, видео, трансляции) должны поддерживать медиа-сессии.
  • Реализовать продвинутые возможности для управления голосом (поиск, переходы между разделами и т. д.) можно с помощью технологии AppFunctions.

Спецификация доступности (accessibility)

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

Что необходимо реализовать

Из рекомендаций по доступности особенно важно реализовать следующие требования:

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

  • Все кликабельные или фокусируемые изображения и иконки должны иметь заданный contentDescription. Например, иконка поиска в верхнем меню должна иметь contentDescription="Search button" или аналогичное описание. Название фильма или сериала, отображаемое в интерфейсе, должно быть также доступно через contentDescription.

Подробнее о том, как реализовать доступность в приложении, можно узнать в официальных гайдах Google для View, Custom View или Jetpack Compose.

Внимание

Явное или неявное использование AndroidView из Compose нарушает совместимость с нашей технологией.

Как проверить

Чтобы проверить текущую разметку в приложении через UIAutomator:

  1. Запустите приложение.

  2. Откройте нужный экран.

  3. Выполните следующую команду:

    adb shell uiautomator dump 
    
  4. Откройте сгенерированный файл /sdcard/window_dump.xml и изучите разметку.

Примеры корректной реализации карусели с карточками фильмов

Откроем экран приложения, на котором отображается карусель с карточками фильмов.

Запустим команду uiautomator, получим XML-файл с описанием интерфейса и найдем в нем, например, второй элемент из карусели:

<node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[708,614][1214,1016]">
    <node index="0" text="" resource-id="ru.kinopoisk.yandex.tv:id/composeView" class="androidx.compose.ui.platform.ComposeView" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[708,614][1214,1016]">
        <node index="0" text="" resource-id="" class="android.view.View" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[708,614][1214,1016]">
            <node index="0" text="" resource-id="" class="android.view.View" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[710,616][1212,1014]">
                <node index="0" text="" resource-id="" class="android.view.View" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[710,616][1212,870]"/>
                <node index="1" text="1 ч 53 мин" resource-id="" class="android.widget.TextView" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[1081,849][1200,870]"/>
                <node index="2" text="" resource-id="" class="android.widget.ProgressBar" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[710,870][1212,918]"/>
                <node index="3" text="Джентльмены" resource-id="" class="android.widget.TextView" package="ru.kinopoisk.yandex.tv" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[710,914][1188,946]"/>
            </node>
        </node>
    </node>
</node>

Можно сделать следующие выводы:

  • элемент кликабелен (clickable="true");

  • у карточки присутствует текстовое описание.

Это означает, что элемент корректно передает информацию в службу доступности, и Алиса сможет его распознать и активировать.

Пример некорректной реализации карусели с карточками фильмов

Откроем экран приложения, на котором отображается карусель с карточками фильмов.

Запустим команду uiautomator, получим XML-файл с описанием интерфейса и найдем в нем, например, первый элемент из карусели:

<node NAF="true" index="5" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[1910,824][1920,1080]">
    <node index="0" text="" resource-id="ru.amediateka:id/poster" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[1910,868][1920,1080]">
        <node index="0" text="" resource-id="ru.amediateka:id/main_image" class="android.widget.ImageView" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[1910,868][1920,1080]"/>
    </node>
</node>

Можно сделать следующие выводы:

  • элемент кликабелен (clickable="true");

  • у карточки отсутствует текстовое описание contentDescription или text.

Это означает, что элемент не передает необходимую информацию в службу доступности, и Алиса не сможет его распознать или активировать.

Пример некорректной реализации элементов бокового меню

Откроем любой экран приложения, на котором отображается боковое меню.

Запустим команду uiautomator, получим XML-файл с описанием интерфейса и найдем в нем элементы бокового меню:

<node index="0" text="" resource-id="ru.amediateka:id/main_menu_fragment" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][84,1080]">
    <node index="0" text="" resource-id="ru.amediateka:id/list" class="androidx.recyclerview.widget.RecyclerView" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,162][84,918]">
        <node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,198][84,282]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,222][66,258]"/></node>
        <node index="1" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,318][84,402]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,342][66,378]"/></node>
        <node index="2" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,438][84,522]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,462][66,498]"/></node>
        <node index="3" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,558][84,642]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,582][66,618]"/></node>
        <node index="4" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,678][84,762]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,702][66,738]"/></node>
        <node index="5" text="" resource-id="" class="android.widget.FrameLayout" package="ru.amediateka" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,798][84,882]">
            <node index="0" text="" resource-id="ru.amediateka:id/icon" class="android.widget.ImageView" package="ru.amediateka" content-desc="Amediateka" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[18,822][66,858]"/>
        </node>
    </node>
</node>

Можно сделать следующие выводы:

  • элементы некликабельны (clickable="false");

  • у элементов отсутствует текстовое описание contentDescription или text.

Элементы нельзя отличить друг от друга и понять, для чего они нужны. Они не передают необходимую информацию в службу доступности, и Алиса не сможет их распознать или активировать.

Медиа-сессии

Для видео-контента необходимо реализовать поддержку медиа-сессии. Это позволяет пользователям Яндекс ТВ Станции управлять воспроизведением — ставить контент на паузу, возобновлять просмотр и перематывать видео.

Чтобы реализовать медиа-сессию перед началом воспроизведения видео-контента, создайте MediaSession и установите callback и флаги:

session = MediaSession(this, "MusicService").apply {
    setCallback(MediaSessionCallback())
    setFlags(
        MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS
    )
}

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

Технология AppFunctions

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

Важно

API находится в активной разработке и может меняться. Ниже представлен обзор реализации технологии AppFunctions.

Доступ к SDK будет предоставлен после одобрения заявки на бета-тестирование.

Описание функций и примеры реализации

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

Ниже приведены примеры основных функций и их описания для онлайн-кинотеатров:

Вызываемая функция Описание
Поиск контента Выполняет поиск аудио, радио, телепередач, каналов, видеоконтента внутри приложения <название приложения>. Если пользователь явно указывает в запросе тип контента (видео, музыка, канал, фильм, сериал, песня, альбом, мультфильм, подкаст, аудиокнига, радио и др.) или провайдера (Кинопоиск, ivi, YouTube и др.), то эта информация должна остаться в тексте запроса. Открывает экран поисковой выдачи, если конкретное видео не может быть воспроизведено или открыто с помощью другой функции.
Включить/выключить субтитры Включает или выключает субтитры во время проигрывания контента в текущем приложении.
Переход в <название раздела> Переключает на раздел <название раздела> в приложении <название приложения>.
Добавить фильм в «Избранное» Добавляет пометку «Избранное» для проигрываемого фильма.

Примечание

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

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

Технические требования

  • compileSdk: версия 36 или выше
  • Kotlin: версия 2.0 или выше
  • Android Gradle plugin: версия 8.9.1 или выше
  • KSP (Kotlin Symbol Processing): должен быть подключен и корректно настроен

Как добавить SDK в проект

Файл SDK поставляется по запросу после одобрения заявки на бета-тестирование. Он представляет собой .aar файл и не содержит внешних зависимостей, поэтому их нужно добавить в проект вручную.

Чтобы реализовать AppFunctions в приложении, необходимо подключить файл SDK к проекту, библиотеку androidx.appfunctions:appfunctions, а также другие внешние зависимости, необходимые для работы SDK.

  1. Поместите .aar файл библиотеки в папку libs, которая должна находиться на одном уровне с папкой src внутри вашего проекта.

  2. Определите версии зависимостей:

    # versions.toml
     [versions]
     appfunctions = "1.0.0-alpha03"
     coroutines = "1.7.3"
     annotation = "1.7.0"
     appsearch = "1.1.0-beta01"
     lifecycle = "2.5.1"
    
     [libraries]
     # androidx
     appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "appfunctions" }
     appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "appfunctions" }
     appfunctions-service = { module = "androidx.appfunctions:appfunctions-service", version.ref = "appfunctions" }
     annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
     appsearch = { module = "androidx.appsearch:appsearch", version.ref = "appsearch" }
     lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
     lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
    
     # other
     coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
     coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
    
  3. Добавьте зависимости в build.gradle.kts:

     dependencies {
         implementation(fileTree("libs"))
    
         ksp(libs.appfunctions.compiler)
    
         implementation(libs.appfunctions)
         implementation(libs.appfunctions.service)
         implementation(libs.appfunctions.compiler)
         implementation(libs.appsearch)
         implementation(libs.coroutines.core)
         implementation(libs.coroutines.android)
         implementation(libs.lifecycle.runtime.ktx)
         implementation(libs.lifecycle.process)
     }
    
  4. В build.gradle.kts приложения включите аггрегацию AppFunctions через настройки ksp :

    android {
        ksp {
            arg("appfunctions:aggregateAppFunctions", "true")
        }
    }
    

    Подробнее про библиотеку androidx.appfunctions можно почитать здесь.

Как добавить AppFunctions в приложение

Добавьте интерфейсы для функций приложения и их реализации. Ниже приведены примеры функций и их интерфейсов для различных приложений.

Описание функции и ее параметров извлекается из KDoc к функции, которая аннотирована @AppFunction.

Важно

Используйте русский язык для описания функций.

Примеры функций для экрана видеоплеера
  1. Определите интерфейсы AddVideoToFavoritesAppFunction, ToggleSubtitlesForVideoAppFunction и ChangeVideoQualityAppFunction для функций addVideoToFavorites, toggleSubtitlesForVideo и changeVideoQuality соответственно:

    VideoPlayerFunctions.kt

    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.AppFunctionSchemaDefinition
    
    const val APP_FUNCTION_SCHEMA_CATEGORY_VIDEO_PLAYER: String = "video_player"
    
    @AppFunctionSchemaDefinition(
        name = "Добавляет текущее видео в избранное.",
        version = AddVideoToFavoritesAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_VIDEO_PLAYER,
    )
    interface AddVideoToFavoritesAppFunction {
        suspend fun addVideoToFavorites(
            appFunctionContext: AppFunctionContext,
        ): Boolean
    
        companion object {
            /** Current schema version. */
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    
    @AppFunctionSchemaDefinition(
        name = "Включает/отключает субтитры в текущем видео.",
        version = ToggleSubtitlesForVideoAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_VIDEO_PLAYER,
    )
    interface ToggleSubtitlesForVideoAppFunction {
        suspend fun toggleSubtitlesForVideo(
            appFunctionContext: AppFunctionContext,
        ): Boolean
    
        companion object {
            /** Current schema version. */
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    
    @AppFunctionSchemaDefinition(
        name = "Меняет качество текущего видео.",
        version = ChangeVideoQualityAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_VIDEO_PLAYER,
    )
    interface ChangeVideoQualityAppFunction {
        suspend fun changeVideoQuality(
            appFunctionContext: AppFunctionContext,
            newQuality: String,
        ): Boolean
    
        companion object {
            /** Current schema version. */
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    

    AddVideoToFavoritesAppFunction

    Добавляет текущее видео в список избранного.

    Параметры:

    Параметр

    Описание

    appFunctionContext

    AppFunctionContext

    Контекст выполнения функции. Предоставляет доступ к системным ресурсам и текущему состоянию приложения.

    Возвращаемое значение:

    Признак успешного выполнения операции (Boolean): true — видео успешно добавлено в список избранного, false — произошла ошибка.

    ToggleSubtitlesForVideoAppFunction

    Включает или выключает субтитры для текущего видео.

    Параметры:

    Параметр

    Описание

    appFunctionContext

    AppFunctionContext

    Контекст выполнения функции. Предоставляет доступ к системным ресурсам и текущему состоянию приложения.

    Возвращаемое значение:

    Признак успешного выполнения операции (Boolean): true — субтитры успешно переключены, false — произошла ошибка.

    ChangeVideoQualityAppFunction

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

    Параметры:

    Параметр

    Описание

    appFunctionContext

    AppFunctionContext

    Контекст выполнения функции. Предоставляет доступ к системным ресурсам и текущему состоянию приложения.

    newQuality

    String

    Качество видео, которое нужно установить. Доступные значения: «240p», «360p», «480p», «720p», «1080p», «2k», «4k».

    Возвращаемое значение:

    Признак успешного выполнения операции (Boolean): true — качество видео успешно изменено, false — произошла ошибка.

  2. Определите класс VideoPlayerFunctionsImpl, который реализует функциональность видеоплеера:

    VideoPlayerFunctionsImpl.kt

    import android.content.Context
    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.service.AppFunction
    
    class VideoPlayerFunctionsImpl: AddVideoToFavoritesAppFunction, ToggleSubtitlesForVideoAppFunction, ChangeVideoQualityAppFunction {
        /**
        * Добавление пометки "Избранное" для проигрываемого контента. Позволяет добавить фильм, cериал, мультфильм или любой
        * другой проигрываемый сейчас контент в "Избранное".
        * 
        * @return Успешно ли видео добавлено в список избранного.
        *
        * */
        @AppFunction(isEnabled = false, isDescribedByKdoc = true)
        override suspend fun addVideoToFavorites(
            appFunctionContext: AppFunctionContext,
        ): Boolean {
            TODO()
        }
    
        /**
        * Включение или выключение субтитров во время проигрывания контента в текущем приложении. Функция может
        * включить или отключить субтитры у текущего видео, фильма, сериала или другого видеоконтента.
        *
        * @return Успешно ли получилось переключить субтитры.
        * */
        @AppFunction(isEnabled = false, isDescribedByKdoc = true)
        override suspend fun toggleSubtitlesForVideo(
            appFunctionContext: AppFunctionContext,
        ): Boolean {
            TODO()
        }
    
        /**
        * Смена качества для текущего проигрываемого контента (видео, сериала, фильма или другого контента). Позволяет поменять
        * качество на минимальное, максимальное или конкретное. Любой запрос стоит приводить к ближайшему
        * значению из списка.
        *
        * @param newQuality Новое качество видео, которое нужно установить.
        * Может быть строго одним из значений: [240p, 360p, 480p, 720p, 1080p, 2k, 4k].
        * @return Успешно ли получилось переключить качество видео.
        * */
        @AppFunction(isEnabled = false, isDescribedByKdoc = true)
        override suspend fun changeVideoQuality(
            appFunctionContext: AppFunctionContext,
            newQuality: String,
        ): Boolean {
            TODO()
        }
    }
    

    Когда система вызовет какую-либо функцию, будет выполнен код из класса VideoPlayerFunctionsImpl, который помечен аннотацией @AppFunction.

Примеры функций для навигации по разделам
  1. Определите интерфейс OpenMenuTabByNameAppFunction для функции openMenuTabByName:

    NavigationFunctions.kt

    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.AppFunctionSchemaDefinition
    
    const val APP_FUNCTION_SCHEMA_CATEGORY_NAVIGATION: String = "navigation"
    
    @AppFunctionSchemaDefinition(
        name = "Открывает указанный раздел в меню приложения.",
        version = OpenMenuTabByNameAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_NAVIGATION,
    )
    interface OpenMenuTabByNameAppFunction {
        suspend fun openMenuTabByName(
            appFunctionContext: AppFunctionContext,
            tabName: String,
        ): Boolean
    
        companion object {
            /** Current schema version. */
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    

    OpenMenuTabByNameAppFunction

    Открывает указанный раздел в меню приложения.

    Параметры:

    Параметр

    Описание

    appFunctionContext

    AppFunctionContext

    Контекст выполнения функции. Предоставляет доступ к системным ресурсам и текущему состоянию приложения.

    tabName

    String

    Название раздела, который нужно открыть. Возможные значения: «Главная», «Подписки», «Еще», «Спорт», «Киберспорт».

    Возвращаемое значение:

    Признак успешного выполнения операции (Boolean): true — раздел меню успешно открыт, false — произошла ошибка.

  2. Определите класс NavigationFunctionsImpl, который реализует возможность навигации по разделам меню приложения:

    NavigationFunctionsImpl.kt

    import android.content.Context
    import android.util.Log
    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.service.AppFunction
    
    class NavigationFunctionsImpl: OpenMenuTabByNameAppFunction, OpenContentByItemSelectionNumber {
    
        /**
        * Открывает вкладку меню или раздел приложения по указанному названию.
        *
        * @param tabName Название раздела, который нужно открыть. Может быть строго одним из значений: [Главная, Подписки, Еще,
        * Спорт, Киберспорт].
        * @return Успешно ли получилось открыть указанный раздел.
        * */
        @AppFunction(isEnabled = true, isDescribedByKdoc = true)
        override suspend fun openMenuTabByName(
            appFunctionContext: AppFunctionContext,
            tabName: String,
        ): Boolean {
            TODO()
        }
    }
    

    Когда система вызовет какую-либо функцию, будет выполнен код из класса NavigationFunctionsImpl, который помечен аннотацией @AppFunction.

Примеры функций для поиска контента в приложении
  1. Определите интерфейс SearchAppFunction для функции contentSearchByQuery:

    SearchFunctions.kt

    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.AppFunctionSchemaDefinition
    
    const val APP_FUNCTION_SCHEMA_CATEGORY_SEARCH: String = "search"
    
    @AppFunctionSchemaDefinition(
        name = "Выполняет поиск видео по указанному названию.",
        version = SearchAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_SEARCH,
    )
    interface SearchAppFunction {
        suspend fun contentSearchByQuery(
            appFunctionContext: AppFunctionContext,
            query: String,
        )
    
        companion object {
            /** Current schema version. */
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    

    SearchAppFunction

    Выполняет поиск видео по указанному названию.

    Параметры:

    Параметр

    Описание

    appFunctionContext

    AppFunctionContext

    Контекст выполнения функции. Предоставляет доступ к системным ресурсам и текущему состоянию приложения.

    query

    String

    Текст запроса для поиска.

  2. Определите класс SearchFunctionsImpl, который реализует возможность поиска видео по названию:

    SearchFunctionsImpl.kt

    import android.content.Context
    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.service.AppFunction
    
    class SearchFunctionsImpl: SearchAppFunction {
    
        /**
        * Поиск аудио, радио, телепередач, каналов, видеоконтента внутри приложения <название приложения>.
        * Если пользователь явно указывает в запросе тип контента (видео, музыка, канал, фильм, сериал, песня, альбом,
        * мультфильм, подкаст, аудиокнига, радио и др.), то его необходимо оставить в тексте запроса.
        * Открывает экран поисковой выдачи, если конкретное видео не будет включено или открыто в рамках другой функции.
        *
        * @param query Текст запроса, по которому нужно произвести поиск.
        * */
        @AppFunction(isEnabled = true, isDescribedByKdoc = true)
        override suspend fun contentSearchByQuery(
            appFunctionContext: AppFunctionContext,
            query: String,
        ) {
            TODO()
        }
    }
    

    Когда система вызовет какую-либо функцию, будет выполнен код из класса SearchFunctionsImpl, который помечен аннотацией @AppFunction.

Примеры функций для создания и хранения заметок

Важно

Передача объектов в функции поддерживается только начиная с Android 12. Поскольку устройства на базе YaOS в настоящее время используют Android 11, данный пример приложения пока не работает.

  1. Определите интерфейс ShowNoteAppFunction для функции showNote, а также интерфейсы для ее параметров:

    ShowNoteFunction.kt

    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.AppFunctionSchemaDefinition
    
    @AppFunctionSchemaDefinition(
        name = "Отображает заметку с заданными параметрами.",
        version = ShowNoteAppFunction.SCHEMA_VERSION,
        category = APP_FUNCTION_SCHEMA_CATEGORY_NOTES
    )
    interface ShowNoteAppFunction<
            Parameters : ShowNoteAppFunction.Parameters,
            Response : ShowNoteAppFunction.Response
            > {
    
        suspend fun showNote(
            appFunctionContext: AppFunctionContext,
            showNoteParams: Parameters
        ): Response
    
        interface Parameters {
            val noteId: String
        }
    
        interface Response {
            val isSuccess: Boolean
        }
    
        companion object {
            internal const val SCHEMA_VERSION: Int = 1
        }
    }
    

    ShowNoteAppFunction

    Отображает заметку с заданными параметрами.

    Параметр

    Описание

    Parameters

    Определяет структуру входных данных для функции showNote. Должен реализовывать вложенный интерфейс ShowNoteAppFunction.Parameters.

    Response

    Определяет структуру выходных данных функции showNote. Должен реализовывать вложенный интерфейс ShowNoteAppFunction.Response.

    Parameters

    Определяет структуру входных данных для функции showNote. Должен реализовывать вложенный интерфейс ShowNoteAppFunction.Parameters.

    Параметр

    Описание

    noteId

    String

    Уникальный идентификатор заметки.

    Response

    Определяет структуру выходных данных функции showNote. Должен реализовывать вложенный интерфейс ShowNoteAppFunction.Response.

    Параметр

    Описание

    isSuccess

    Boolean

    Флаг успешного выполнения операции: true — заметка успешно отображена, false — произошла ошибка при выполнении.

  2. Определите классы ShowNoteParameters и ShowNoteResponse для конкретной реализации параметров интерфейса:

    ShowNoteParams.kt

    @AppFunctionSerializable
    data class ShowNoteParameters(
        override val noteId: String
    ): ShowNoteAppFunction.Parameters
    
    @AppFunctionSerializable
    data class ShowNoteResponse(
        override val isSuccess: Boolean
    ): ShowNoteAppFunction.Response
    
  3. Определите класс NoteFunctionsImpl, который реализует функциональность отображения заметок в приложении:

    NoteFunctionsImpl.kt

    import android.content.Context
    import androidx.appfunctions.AppFunctionContext
    import androidx.appfunctions.service.AppFunction
    
    class NoteFunctionsImpl: ShowNoteAppFunction<ShowNoteParameters, ShowNoteResponse> {
        /**
        * Отображает заметку с заданными параметрами.
        *
        * @param showNoteParams Входные данные: уникальный идентификатор заметки.
        * @return Успешно ли отображена заметка.
        * */
        @AppFunction(isEnabled = true)
        override suspend fun showNote(
            appFunctionContext: AppFunctionContext,
            showNoteParams: ShowNoteParameters,
        ): ShowNoteResponse {
            TODO()
        }
    }
    

    Когда система вызовет какую-либо функцию, будет выполнен код из класса NoteFunctionsImpl, который помечен аннотацией @AppFunction.

Как управлять функциями приложения

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

Важно

Экземпляр класса com.yandex.tv.features.appfunctions.sdk.AppFunctionSdk необходимо создать, даже если не планируется управление AppFunctions (т. е. присутствуют только функции, которые isEnabled=true по умолчанию).

С помощью класса AppFunctionSdk можно вызывать следующие методы:

  • Управление конкретной функцией
suspend fun enableAppFunction(functionIdentifier: String)
suspend fun disableAppFunction(functionIdentifier: String)

Параметр

Описание

functionIdentifier

String

Уникальный идентификатор функции (например, addVideoToFavorites).

  • Групповое управление по категориям
suspend fun enableAppFunctionsBySchemaCategory(schemaCategory: String)
suspend fun disableAppFunctionsBySchemaCategory(schemaCategory: String)

Параметр

Описание

schemaCategory

String

Категория для группы связанных функций (например, video_player).

  • Управление функциями или группой функций с привязкой к конкретному LifecycleOwner
fun AppFunctionSdk.bindFunctionToLifecycle(functionIdentifier: String, lifecycleOwner: LifecycleOwner)
fun AppFunctionSdk.bindFunctionsToLifecycle(schemaCategory: String, lifecycleOwner: LifecycleOwner)

Параметр

Описание

functionIdentifier

String

Уникальный идентификатор функции (например, addVideoToFavorites).

schemaCategory

String

Категория для группы связанных функций (например, video_player).

lifecycleOwner

LifecycleOwner

Компонент имеющий жизненный цикл (Activity или Fragment).

Функции автоматически включаются и выключаются в состоянии Lifecycle.State.STARTED. Например, для Activity это происходит после вызова onStart() и перед вызовом onPause().

Примечание

Все идентификаторы функций functionIdentifier будут доступны в сгенерированном статичном объекте с названием <Название класса с аннотациями @AppFunction>Ids. Например, для класса с функциями видеоплеера будет сгенерирован следующий объект:

@Generated("androidx.appfunctions.compiler.AppFunctionCompiler")
public object VideoPlayerFunctionsImplIds {
  public const val ADD_VIDEO_TO_FAVORITES_ID: String =
      "com.yandex.tv.features.appfunctions.sdk.sample.appfunctions.player.VideoPlayerFunctionsImpl#addVideoToFavorites"

  public const val TOGGLE_SUBTITLES_FOR_VIDEO_ID: String =
      "com.yandex.tv.features.appfunctions.sdk.sample.appfunctions.player.VideoPlayerFunctionsImpl#toggleSubtitlesForVideo"

  public const val CHANGE_VIDEO_QUALITY_ID: String =
      "com.yandex.tv.features.appfunctions.sdk.sample.appfunctions.player.VideoPlayerFunctionsImpl#changeVideoQuality"
}