语音助理点读功能

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

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

点读通知

在 Automotive 中,发布到通知中心并被标识为 INBOX 或 INBOX_IN_GROUP 通知(如短信)的通知将包含一个“播放”操作按钮。通过此按钮,用户可以用选定的 VIA 朗读通知,并可以选择通过语音回复。

图 1. 点读通知

CarVoiceInteractionSession 集成

1. 支持 VoiceInteractions

提供车载设备语音互动服务的应用必须与现有的 Android 语音互动服务集成(VoiceInteractionSession 除外)。虽然 Voice Interaction API 中的所有其他组件都与移动设备上的实现相同,但 CarVoiceInteractionSession(如下所述)取代了 VoiceInteractionSession。如需了解详情,请参阅以下文章:

2. 实现 CarVoiceInteractionSession

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

对比 CarVoiceInteractionSessionVoiceInteractionSession 就会发现,它们主要的区别在于,CarVoiceInteractionSession 会在 onShow 中传入操作,因此语音助理能够在 CarVoiceInteractionSession 启动会话后立即检测到用户请求的背景信息。

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,则说明能够处理用户请求的后备助理已停用。
需要针对异常采取的预期操作将在相应异常的文档中指定。

ExceptionValue

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

3. 请求通知监听器权限

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

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

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

4. 使用 StatusBarNotification

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

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

5. 满足点读前提条件

当用户触发朗读和回复消息的语音操作时,只有默认语音助理的 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 applications, so check this
    // field for a system application 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;
}