语音助理点读功能

Android Automotive 认为语音是在确保安全驾驶的同时进行互动的关键组件,也是用户在驾车期间与 Android Automotive OS 互动的最安全方式之一。因此,我们扩展了 Android 语音助理 API(包括 VoiceInteractionSession),使语音助理能够为用户执行那些在驾车时难以完成的任务。

利用点读功能,语音助理能够在用户与消息通知互动时朗读短信,并代表用户进行回复。如需提供此功能,您可以将语音助理与 CarVoiceInteractionSession 集成。

在 Automotive 中,发布到通知中心并被标识为 INBOXINBOX_IN_GROUP(如短信)的通知将包含一个播放按钮。用户可以点击播放,让选定的语音助理朗读通知,并可以选择通过语音回复。

点读通知

图 1. 包含“播放”按钮的点读通知。

与 CarVoiceInteractionSession 集成

接下来的部分会介绍如何将语音助理与 CarVoiceInteractionSession 集成。

支持语音交互

提供车载语音交互服务的应用必须与现有的 Android 语音交互服务集成。如需了解详情,请参阅 Android 版 Google 助理VoiceInteractionSession 除外)。虽然所有语音交互 API 元素都与在移动设备上实现时相同,但 CarVoiceInteractionSession(如实现 CarVoiceInteractionSession 中所述)取代了 VoiceInteractionSession。如需了解详情,请参阅以下页面:

实现 CarVoiceInteractionSession

CarVoiceInteractionSession 提供了多个 API,通过这些 API,语音助理可以朗读短信,随后再代表用户进行回复。

CarVoiceInteractionSession 类和 VoiceInteractionSession 类之间的主要区别在于,CarVoiceInteractionSession 会在 onShow 中传入操作,因此语音助理能够在 CarVoiceInteractionSession 启动会话后立即检测到用户请求的背景信息。下表列出了每个类的 onShow 的参数:

CarVoiceInteractionSession VoiceInteractionSession
onShow 采用以下三个参数
  • args
  • showFlags
  • actions
onShow 采用以下两个参数
  • args
  • showFlags

Android 10 中的变化

从 Android 10 开始,平台会调用 VoiceInteractionService.onGetSupportedVoiceActions 来检测哪些操作受到支持。语音助理会替换并实现 VoiceInteractionService.onGetSupportedVoiceActions,如以下示例所示:

public class MyInteractionService extends VoiceInteractionService {
    private static final List SUPPORTED_VOICE_ACTIONS = Arrays.asList(
        CarVoiceInteractionSession.VOICE_ACTION_READ_NOTIFICATION);

    @Override
    public Set onGetSupportedVoiceActions(@NonNull Set voiceActions) {
       Set result = new HashSet<>(voiceActions);
       result.retainAll(SUPPORTED_VOICE_ACTIONS);
       return result;
   }
}

有效操作如下方表格中所述。如需详细了解各项操作,请参阅序列图

操作 预期载荷 预期语音交互操作
VOICE_ACTION_READ_NOTIFICATION 向用户朗读消息,然后在成功朗读完消息后触发“标记为已读”待处理 intent。(可选)提示用户回复。
VOICE_ACTION_REPLY_NOTIFICATION 包含键的 Parcelable。
KEY_NOTIFICATION(映射到 StatusBarNotification)。
需要 android.permission.BIND_NOTIFICATION_LISTENER_SERVICE 权限
提示用户讲出回复消息,将回复消息输入到待处理 intent 的 RemoteInputReply 中,然后触发该待处理 intent。
VOICE_ACTION_HANDLE_EXCEPTION 包含键的字符串。
KEY_EXCEPTION(映射到 ExceptionValue,如异常值中所述)。
KEY_FALLBACK_ASSISTANT_ENABLED(映射到某个布尔值)。如果该布尔值为 true,则说明能够处理用户请求的后备助理已停用。
需要针对异常采取的预期操作在相应异常的文档中指定。

异常值

EXCEPTION_NOTIFICATION_LISTENER_PERMISSIONS_MISSING 指示语音助理它缺少 Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE 权限,应向用户请求此权限。

请求通知监听器权限

如果默认语音助理没有通知监听器权限,平台的 FallbackAssistant(如果已由汽车制造商启用)可能会在系统通知语音助理请求相应权限之前朗读消息。为了确定 FallbackAssistant 是否启用并已经朗读了消息,语音助理应检查载荷中的 KEY_FALLBACK_ASSISTANT_ENABLED 布尔值。

平台建议语音助理为此权限的请求次数添加速率限制逻辑。如果用户不想向语音助理授予此权限,而更希望让 FallbackAssistant 朗读短信,那么这样做是对用户的尊重。如果每次用户按下消息通知上的播放时,系统都提示用户授予权限,可能会导致用户体验不佳。平台不会代表语音助理实施速率限制。

在请求通知监听器权限时,语音助理应使用 CarUxRestrictionsManager 来确定用户已停车还是正在驾车。如果用户正在驾车,语音助理会显示一条通知,说明如何授予该权限。这样做有助于(并提醒)用户在更安全的时候授予权限。

使用 StatusBarNotification

随“朗读”和“回复”语音操作传入的 StatusBarNotification 总是包含在与车载设备兼容的消息通知中,如向用户提供消息通知中所述。虽然某些通知可能没有“回复”待处理 intent,但所有通知都有“标记为已读”待处理 intent。

如需简化与通知的互动,请使用 NotificationPayloadHandler,它提供的多种方法可以从通知中提取消息,并将回复消息写入通知的相应待处理 intent 中。语音助理朗读完消息后,必须触发“标记为已读”intent。

满足点读前提条件

当用户触发朗读和回复消息的语音操作时,只有默认语音助理的 VoiceInteractionSession 才会收到通知。如上所述,此默认语音助理还必须拥有通知监听器权限。

序列图

以下各图展示了 CarVoiceInteractionSession actions 的逻辑流:

VOICE_ACTION_READ_NOTIFICATION

图 2. VOICE_ACTION_READ_NOTIFICATION 的序列图。

如果遇到图 3 中的情况,建议对权限请求应用速率限制。

VOICE_ACTION_REPLY_NOTIFICATION

图 3. VOICE_ACTION_REPLY_NOTIFICATION 的序列图。

VOICE_ACTION_HANDLE_EXCEPTION

图 4. VOICE_ACTION_HANDLE_EXCEPTION 的序列图。

读出应用名称

如果想让语音助理在朗读消息时读出即时通讯应用的名称(比如,“小王在 Hangouts 中说…”),请创建如以下代码示例所示的函数,以确保语音助理读出正确的名称:

@Nullable
String getMessageApplicationName(Context context, StatusBarNotification statusBarNotification) {
    ApplicationInfo info = getApplicationInfo(context, statusBarNotification.getPackageName());
    if (info == null) return null;

    Notification notification = statusBarNotification.getNotification();

    // Sometimes system packages will post on behalf of other apps, so check this
    // field for a system app notification.
    if (isSystemApp(info)
            && notification.extras.containsKey(Notification.EXTRA_SUBSTITUTE_APP_NAME)) {
        return notification.extras.getString(Notification.EXTRA_SUBSTITUTE_APP_NAME);
    } else {
        PackageManager pm = context.getPackageManager();
        return String.valueOf(pm.getApplicationLabel(info));
    }
}

@Nullable
ApplicationInfo getApplicationInfo(Context context, String packageName) {
    final PackageManager pm = context.getPackageManager();
    ApplicationInfo info;
    try {
        info = pm.getApplicationInfo(packageName, 0);
    } catch (PackageManager.NameNotFoundException e) {
        return null;
    }
    return info;
}

boolean isSystemApp(ApplicationInfo info) {
    return (info.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
}