将 App Actions 与 Android 小部件集成

图 1. 启动 GET_EXERCISE_OBSERVATION 的小部件。

对于许多意图,最佳响应是向用户提供简单的答案、简短的确认或快速的交互式体验。您可以在 Google 助理中显示Android 应用小部件 来满足这些类型的意图。

本指南介绍如何使用小部件满足助理用户的查询,以及如何使用 App Actions 小部件扩展库 增强您的小部件在助理中的体验。

优势

小部件是微型应用程序视图,可以嵌入到 Android 表面(例如启动器或锁定屏幕)中。借助 App Actions,您可以通过使小部件有资格在助理中显示来提高其影响力。

  1. 发现:主动显示响应用户自然语言查询的小部件。
  2. 互动:在免提环境中显示小部件,例如当助理在锁定屏幕上提供个人结果时,以及在Android Auto上。
  3. 保留:允许用户将助理中显示的小部件固定到其启动器。固定功能需要小部件扩展库

助理如何显示小部件

用户可以通过两种方式在助理上调用小部件

  • 明确请求指定名称的小部件。
  • 向助理发出查询,触发配置为小部件满足项的内置意图(BII) 或自定义意图

显式调用

要显式调用任何已安装应用的小部件,用户可以向助理询问以下内容:

  • “嘿 Google,显示 ExampleApp 小部件。”
  • “来自 ExampleApp 的小部件。”

助理会显示这些小部件,并带有通用介绍:“ExampleApp 表示,这是一个小部件。”虽然助理本机返回以这种方式请求的小部件,无需应用开发者执行任何操作,但这调用方法要求用户明确知道要请求的小部件。要简化小部件发现,请使用下一节中详细介绍的意图满足方法。

意图满足

通过使用小部件来满足用户在助理上执行的自然语言查询,使您的的部件更容易被发现。例如,当用户在健身应用中触发GET_EXERCISE_OBSERVATION BII 时(例如询问“嘿 Google,本周我在 ExampleApp 上跑了多少英里?”),您可以返回一个小部件。除了简化发现之外,将小部件与 App Actions 集成还具有以下优势:

  • 参数访问:助理会将从用户查询中提取的意图参数 提供给您的部件,从而实现定制响应
  • 自定义 TTS 介绍:您可以提供文本转语音 (TTS) 字符串,供助理在显示您的部件时宣布。
  • 小部件固定:助理会在您的部件附近显示一个“添加此部件”按钮,让用户可以轻松地将其部件固定到启动器。

实现小部件满足

要为您的意图实现小部件满足,请按照以下步骤操作:

  1. 按照创建简单小部件中所述的步骤实现 Android 小部件。
  2. 在应用的 shortcuts.xml 资源文件中,添加一个 <app-widget> 元素到你的功能中,包含执行详情和 BII <parameter> 标签。更新你的小部件以处理参数。
  3. 添加必需的 Widgets Extension 库,它允许 Assistant 将 BII 名称和参数传递到你的小部件。它还支持 自定义 TTS 介绍 和小部件 固定 功能。

以下部分描述了 shortcuts.xml<app-widget> 模式。

小部件模式

<app-widget> 元素在 shortcuts.xml 中的 <capability> 元素内定义为执行。除非另有说明为可选,否则它们需要以下属性。

`shortcuts.xml` 标签包含在属性
<app-widget> <capability>
  • android:identifier
  • android:targetClass
<parameter> <app-widget>
<extra> <app-widget>
  • android:name(仅适用于 TTS)
  • android:value(可选)

小部件模式描述

<app-widget>

顶级小部件执行元素。

属性

  • android:identifier:此执行的标识符。此值在 <capability> 内定义的 <app-widget><intent> 执行元素中必须唯一。
  • android:targetClass:要处理 intent 的 AppWidgetProvider 的完整类名。

<parameter>

将 BII 参数映射到 intent <parameter> 值。你可以为每个 <app-widget> 元素定义零个或多个参数。在执行过程中,Assistant 通过更新小部件实例的额外内容作为键值对来传递参数,格式如下:

  • 键:为参数定义的 android:key
  • 值:BII 从用户的语音输入中提取的值。

你可以通过在关联的 AppWidgetManager 对象上调用 getAppWidgetOptions() 来访问这些额外内容,它返回一个包含触发 BII 的名称及其参数的 Bundle。有关详细信息,请参阅 提取参数值

有关 BII 参数匹配的更多信息,请参阅 参数数据和匹配

<extra>

声明此小部件使用 自定义 TTS 介绍 的可选标签。此标签需要以下属性值:

  • android:name"hasTts"
  • android:value"true"

示例代码

来自 shortcuts.xml 文件的以下示例演示了 GET_EXERCISE_OBSERVATION BII 功能的小部件执行配置。

<capability android:name="actions.intent.GET_EXERCISE_OBSERVATION">
  <app-widget
    android:identifier="GET_EXERCISE_OBSERVATION_1"
    android:targetClass="com.exampleapp.providers.exampleAppWidgetProvider"
    android:targetPackage="com.exampleapp">
    <parameter
      android:name="exerciseObservation.aboutExercise.name"
      android:key="exercisename">
    </parameter>
    <extra android:name="hasTts" android:value="true"/>
  </app-widget>
</capability>

你可以指定多个 <app-widget> 元素,或者组合使用 <app-widget><intent> 元素。这种方法允许你根据用户提供的不同参数组合提供自定义体验。例如,如果用户在查询中未指定下车地点,则可以将其引导至应用中显示设置上车和下车地点选项的活动。有关定义回退 intent 的更多信息,请参阅 回退 intent 部分。

提取参数值

在以下示例 AppWidgetProvider 类中,私有函数 updateAppWidget() 用于从 widget 选项 Bundle 中提取 BII 名称和参数。

Kotlin

package com.example.exampleapp

//... Other module imports
import com.google.assistant.appactions.widgets.AppActionsWidgetExtension

/**
 * Implementation of App Widget functionality.
 */
class MyAppWidget : AppWidgetProvider() {
    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        // There might be multiple widgets active, so update all of them
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    private fun updateAppWidget(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int
    ) {
        val widgetText: CharSequence = context.getString(R.string.appwidget_text)

        // Construct the RemoteViews object
        val views = RemoteViews(context.packageName, R.layout.my_app_widget)
        views.setTextViewText(R.id.appwidget_text, widgetText)

        // Extract the name and parameters of the BII from the widget options
        val optionsBundle = appWidgetManager.getAppWidgetOptions(appWidgetId)
        val bii = optionsBundle.getString(AppActionsWidgetExtension.EXTRA_APP_ACTIONS_BII) // "actions.intent.CREATE_TAXI_RESERVATION"
        val params = optionsBundle.getBundle(AppActionsWidgetExtension.EXTRA_APP_ACTIONS_PARAMS)
        if (params != null && params.containsKey("dropoff")) {
            val dropoffLocation = params.getString("dropoff")
            // Build your RemoteViews with the extracted BII parameter
            // ...
        }
        appWidgetManager.updateAppWidget(appWidgetId, views)
    }
}

Java

package com.example.exampleapp;

//... Other module imports
import com.google.assistant.appactions.widgets.AppActionsWidgetExtension;

/**
 * Implementation of App Widget functionality.
 */
public class MyAppWidget extends AppWidgetProvider {

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There might be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);

        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.my_app_widget);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Extract the name and parameters of the BII from the widget options
        Bundle optionsBundle = appWidgetManager.getAppWidgetOptions(appWidgetId);
        String bii =
                optionsBundle.getString(AppActionsWidgetExtension.EXTRA_APP_ACTIONS_BII); // "actions.intent.CREATE_TAXI_RESERVATION"
        Bundle params =
                optionsBundle.getBundle(AppActionsWidgetExtension.EXTRA_APP_ACTIONS_PARAMS);

        if (params != null && params.containsKey(("dropoff"))){
            String dropoffLocation = params.getString("dropoff");
            // Build your RemoteViews with the extracted BII parameter
            // ...
        }

        appWidgetManager.updateAppWidget(appWidgetId, views);
    }
}

Widgets Extension 库

App Actions Widgets Extension 库增强了你的小部件,使其能够支持面向语音的 Assistant 体验。此库允许你的小部件从触发 BII 中接收重要的执行信息,包括 BII 名称和从用户查询中提取的任何 intent 参数。

此 Maven 库允许你为每个小部件提供自定义文本转语音 (TTS) 介绍,使 Assistant 能够宣布向用户视觉呈现的内容摘要。它还支持 启动器固定,使用户可以轻松地将 Assistant 中显示的小部件保存到其启动器屏幕。

首先将库添加到应用模块的 build.gradle 文件的依赖项部分。

dependencies {
    //...
    implementation "com.google.assistant.appactions:widgets:0.0.1"
}

自定义介绍

导入 Widgets Extension 库后,你可以为你的小部件提供自定义 TTS 介绍。要将你的定义添加到小部件的 AppWidgetProvider,请在 IDE 中打开该类并导入 Widgets Extension 库。

Kotlin

import com.google.assistant.appactions.widgets.AppActionsWidgetExtension

Java

import com.google.assistant.appactions.widgets.AppActionsWidgetExtension;
接下来,使用该库定义你的介绍字符串并更新小部件,如以下 `ExampleAppWidget` 中所示。

Kotlin

package com.example.exampleapp

//... Other module imports
import com.google.assistant.appactions.widgets.AppActionsWidgetExtension

/**
 * Implementation of App Widget functionality.
 */
object MyAppWidget : AppWidgetProvider() {
    fun updateAppWidget(
        context: Context?,
        appWidgetManager: AppWidgetManager,
        appWidgetId: Int
    ) {
        val appActionsWidgetExtension = AppActionsWidgetExtension.newBuilder(appWidgetManager)
            .setResponseSpeech("Hello world") // TTS to be played back to the user
            .setResponseText("Hello world!") // Response text to be displayed in Assistant
            .build()

        // Update widget with TTS
        appActionsWidgetExtension.updateWidget(appWidgetId)

        // Update widget UI
        appWidgetManager.updateAppWidget(appWidgetId, views)
    }
}

Java

package com.example.exampleapp;

//... Other module imports
import com.google.assistant.appactions.widgets.AppActionsWidgetExtension;

/**
 * Implementation of App Widget functionality.
 */
public class MyAppWidget extends AppWidgetProvider {

  static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
    int appWidgetId) {

    AppActionsWidgetExtension appActionsWidgetExtension = AppActionsWidgetExtension.newBuilder(appWidgetManager)
      .setResponseSpeech("Hello world")  // TTS to be played back to the user
      .setResponseText("Hello world!")  // Response text to be displayed in Assistant
      .build();

      // Update widget with TTS
      appActionsWidgetExtension.updateWidget(appWidgetId);

      // Update widget UI
      appWidgetManager.updateAppWidget(appWidgetId, views);
    }

}

TTS 风格建议

使用以下风格建议来优化你的自定义小部件介绍,使其适用于 TTS 和显示的提示。

建议 建议 不建议
缩写
在 TTS 提示中使用缩写。不使用缩写的消息听起来生硬且像机器人,而不是自然和对话式的。说“cannot”和“do not”这样的词听起来可能会严厉和刺耳。
ResponseSpeech (TTS)
对不起,我找不到预订。

ResponseText
对不起,我找不到预订。
ResponseSpeech (TTS)
对不起,我找不到预订。

ResponseText
对不起,我找不到预订。
逗号
在三个或更多项目列表中使用串行逗号以提高清晰度。如果没有串行逗号,列表中的各个项目可能会被错误地听到或读作组。例如,在“水仙花、雏菊和向日葵”中,“雏菊和向日葵”听起来像是组合在一起的。在“水仙花、雏菊和向日葵”中,所有三个都是清晰分开的。
ResponseSpeech (TTS)
我们最受欢迎的产品包括黄玫瑰、水仙花、雏菊和向日葵。

ResponseText
我们最受欢迎的产品包括黄玫瑰、水仙花、雏菊和向日葵。
ResponseSpeech (TTS)
我们最受欢迎的产品包括黄玫瑰、水仙花、雏菊和向日葵。

ResponseText
我们最受欢迎的产品包括黄玫瑰、水仙花、雏菊和向日葵。
数字
使用数字而不是文本,使视觉内容更容易一目了然。
ResponseSpeech (TTS)
你的血压是 100/80。

ResponseText
你的血压是 100/80。
ResponseSpeech (TTS)
你的血压是 100/80。

ResponseText
你的血压是一百除以八十。
符号
使用专用符号而不是文本,使视觉内容更容易一目了然。
ResponseSpeech (TTS)
你上次购买的价格是 24.65 美元。

ResponseText
你上次购买的价格是 24.65 美元。
ResponseSpeech (TTS)
你上次购买的价格是二十四美元六十五美分。

ResponseText
你上次购买的价格是二十四美元六十五美分。
避免客套话
客套话会使回复显得疏远和正式。放弃它们,保持对话友好和非正式。
ResponseSpeech (TTS)
你的订单已送达。

ResponseText
你的订单已送达。
ResponseSpeech (TTS)
当然,我可以告诉你。你的订单已送达。

ResponseText
当然,我可以告诉你。你的订单已送达。
避免使用感叹号
它们可能会被认为是大喊。
ResponseSpeech (TTS)
你今天跑了 1.5 英里。

ResponseText
你今天跑了 1.5 英里。
ResponseSpeech (TTS)
你今天跑了 1.5 英里!

ResponseText
你今天跑了 1.5 英里!
时间
使用数字:“5:15”,而不是“五点十五分”或“五点一刻”。对于 12 小时制时钟,使用 AM 或 PM。
ResponseSpeech (TTS)
你的送货应该在上午 8:15 到达。

ResponseText
你的送货应该在上午 8:15 到达。
ResponseSpeech (TTS)
你的送货应该在今天早上 8 点 15 分到达。

ResponseText
你的送货应该在今天早上 8 点 15 分到达。
不要滔滔不绝
要提供信息,但要保持回复简洁。如果没有明确的用户利益,不要进行过于详细的阐述。
ResponseSpeech (TTS)
上个月你使用了 159 小时的能源。

ResponseText
上个月你使用了 159 小时的能源。
ResponseSpeech (TTS)
节约能源对地球和环境非常重要。上个月你使用了 159 小时的能源。本月你使用了 58 小时的能源。

ResponseText
节约能源对地球和环境非常重要。上个月你使用了 159 小时的能源。本月你使用了 58 小时的能源。
使用简短、简单的词语
简洁明了的语言具有最广泛的吸引力,使其能够被各种背景的人们所理解。
ResponseSpeech (TTS)
你上次的血糖读数是 126。

ResponseText
你上次的血糖读数是 126 mg/dL。
ResponseSpeech (TTS)
倒数第二个血糖水平是 126。

ResponseText
倒数第二个血糖水平是 126。

启动器固定

Widgets Extension 库允许在 Assistant 中的小部件中显示“添加此小部件”按钮。要启用固定功能,请将以下接收器定义添加到 AndroidManifest.xml

<application>
  <receiver android:name="com.google.assistant.appactions.widgets.pinappwidget.PinAppWidgetBroadcastReceiver"
    android:exported="false">
    <intent-filter>
      <action android:name="com.google.assistant.appactions.widgets.COMPLETE_PIN_APP_WIDGET" />
    </intent-filter>
  </receiver>
  <service
    android:name=
    "com.google.assistant.appactions.widgets.pinappwidget.PinAppWidgetService"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
      <action
        android:name="com.google.assistant.appactions.widgets.PIN_APP_WIDGET" />
    </intent-filter>
  </service>
</application>

库存可用性

支持 内联库存网页库存 的 BII 可以将这些库存扩展到你的小部件执行。

内联库存

来自示例 shortcuts.xml 文件的以下代码演示了为内联库存和小部件执行配置的 START_EXERCISE BII 功能。

<capability
  android:name="actions.intent.START_EXERCISE">
  <app-widget
    android:identifier="START_EXERCISE_1"
    android:targetClass="com.example.exampleapp.StartExerciseAppWidgetProvider">
    <parameter
      android:name="exercise.name"
      android:key="exerciseName"
      app:shortcutMatchRequired="true">
    </parameter>
  </app-widget>
</capability>

<shortcut android:shortcutId="RunningShortcut">
  <intent
    android:action="android.intent.action.VIEW"
    android:targetClass="com.example.exampleapp.StartExcerciseActivity" />
  <capability-binding
    android:capability="actions.intent.START_EXERCISE"
    android:parameter="exercise.name"
    android:value="running;runs" />
</shortcut>

在前面的示例中,当用户通过询问 Assistant“开始使用 ExampleApp 跑步”来触发此功能时,<app-widget> 执行的选项包包含以下键值对:

  • 键 = “exerciseName”
  • 值 = “RunningShortcut”

网页库存

来自示例 shortcuts.xml 文件的以下代码显示了为网页库存和小部件执行启用的功能。

<shortcuts>
  <capability
    android:name="actions.intent.START_EXERCISE">
    <app-widget
      android:identifier="START_EXERCISE_1"
      android:targetClass="com.example.exampleapp.CreateTaxiAppWidgetProvider">
      <parameter
        android:name="exercise.name"
        android:key="exerciseName"
        android:mimeType="text/*">
        <data android:pathPattern="https://exampleapp.com/exercise/.*" />
      </parameter>
    </app-widget>
  </capability>
</shortcuts>

测试 App Actions

使用 App Actions 测试工具(Android Studio 的 Google Assistant 插件 的一项功能)在物理设备或虚拟设备上测试小部件。要使用测试工具,请按照以下步骤操作:

  1. 连接运行你的应用的测试设备。
  2. 在 Android Studio 中,转到**工具** > **App Actions** > **App Actions 测试工具**。
  3. 单击**创建预览**。
  4. 使用 Android Studio,在你的测试设备上运行你的应用。
  5. 使用测试设备上的 Assistant 应用测试你的 App Action。例如,你可以说类似于“嘿 Google,我本周在 ExampleApp 上跑了多少英里?”的内容。
  6. 观察你的应用的行为,或使用 Android Studio 调试器 验证所需的操作结果。

质量指南

本节重点介绍将 App Actions 与小部件集成时的关键要求和最佳实践。

小部件中的内容

  • (**必填**) 不要在你的小部件中显示广告。
  • 将小部件内容完全集中在满足 intent 上。不要尝试使用一个小部件来满足多个 intent,也不要添加不相关的内容。

处理身份验证

  • (必填) 在需要用户身份验证才能完成用户流程的情况下,返回一个小部件,说明用户需要在应用中继续操作。Google 助理中不支持内联用户身份验证,此项不适用于应用操作。
  • 如果用户允许您的应用使用小部件显示数据,则您可以在运行时为未授权用户返回错误小部件。

后备意图

  • (必填) 在您的shortcuts.xml文件中,除了给定功能的小部件实现外,始终提供一个后备<intent>。后备意图是一个没有必需<parameter>值的<intent>元素。

    这使得助理能够在用户查询不包含该功能中定义的其他实现元素所需的的参数时,完成操作。例外情况是,当该功能没有必需参数时,只需小部件实现即可。

  • 使用后备意图打开您的应用的相关屏幕,而不是主屏幕。

以下代码摘自示例shortcuts.xml文件,它演示了一个带有后备<intent><capability>,支持主要的<app-widget>实现

<shortcuts>
  <capability
    android:name="actions.intent.CREATE_TAXI_RESERVATION">
    <!-- Widget with required parameter, specified using the "android:required" attribute. -->
    <app-widget
      android:identifier="CREATE_TAXI_RESERVATION_1"
      android:targetClass="com.example.myapplication.CreateTaxiAppWidgetProvider">
      <parameter
        android:name="taxiReservation.dropoffLocation.name"
        android:key="dropoff"
        android:required="true">
      </parameter>
    </app-widget>
    <!-- Fallback intent with no parameters required to successfully execute. -->
    <intent
      android:identifier="CREATE_TAXI_RESERVATION_3"
      android:action="myapplication.intent.CREATE_TAXI_RESERVATION_1"
      android:targetClass="com.example.myapplication.TaxiReservationActivity">
    </intent>
  </capability>
</shortcuts>

Google Play 数据披露

本节列出了小部件扩展库最新版本收集的最终用户数据。

此 SDK 发送开发者提供的文本转语音 (TTS) 响应,这些响应由 Google 助理使用助理的语音技术向用户宣布。此信息不会由 Google 存储。

应用操作 也可能出于以下目的收集客户端应用元数据

  • 监控不同 SDK 版本的采用率。
  • 量化应用中 SDK 功能的使用情况。