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,语音助理可以朗读短信,随后再代表用户进行回复。
对比 CarVoiceInteractionSession
和 VoiceInteractionSession
就会发现,它们主要的区别在于,CarVoiceInteractionSession
会在 onShow
中传入操作,因此语音助理能够在 CarVoiceInteractionSession
启动会话后立即检测到用户请求的背景信息。
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,则说明能够处理用户请求的后备助理已停用。 |
需要针对异常采取的预期操作将在相应异常的文档中指定。 |
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
的逻辑流。
图 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 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; }