Android Automotive 认为语音是在确保安全驾驶的同时进行互动的关键组件,也是用户在驾车期间与 Android Automotive OS 互动的最安全方式之一。因此,我们扩展了 Android 语音助理 API(包括 VoiceInteractionSession
),使语音助理能够为用户执行那些在驾车时难以完成的任务。
利用点读功能,语音助理能够在用户与消息通知互动时朗读短信,并代表用户进行回复。如需提供此功能,您可以将语音助理与 CarVoiceInteractionSession
集成。
在 Automotive 中,发布到通知中心并被标识为 INBOX
或 INBOX_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 采用以下三个参数:
|
onShow 采用以下两个参数:
|
Android 10 中的变化
从 Android 10 开始,平台会调用 VoiceInteractionService.onGetSupportedVoiceActions
来检测哪些操作受到支持。语音助理会替换并实现 VoiceInteractionService.onGetSupportedVoiceActions
,如以下示例所示:
public class MyInteractionService extends VoiceInteractionService { private static final ListSUPPORTED_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
的逻辑流:
图 2. VOICE_ACTION_READ_NOTIFICATION 的序列图。
如果遇到图 3 中的情况,建议对权限请求应用速率限制。
图 3. VOICE_ACTION_REPLY_NOTIFICATION 的序列图。
图 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; }