执行命令

了解如何通过语音互动执行以下类型的命令:

执行媒体命令

与媒体相关的命令可以分为三类:

  • 外部媒体来源(例如安装在 AAOS 中的 Spotify)。
  • 后端媒体来源(例如通过 VIA 在线播放的音乐)。
  • 本地媒体来源(例如车载电台)。

处理外部媒体来源命令

外部媒体来源定义为支持 MediaSessionCompat API 和 MediaBrowseCompat API 的 Android 应用(如需详细了解这些 API 的用法,请参阅构建车载媒体应用)。

重要提示:如需将助理应用连接到系统中安装的所有媒体应用的 MediaBrowseService,它必须:

  1. 安装为系统签名应用(请参阅媒体应用开发指南,获取 AAOS 和 PackageValidator 示例代码)。
  2. 具有 android.permission.MEDIA_CONTENT_CONTROL 系统特许权限(请参阅授予系统特许权限)。

除了 MediaBrowserCompatMediaControllerCompat 之外,AAOS 还提供以下组件:

  • CarMediaService 提供有关当前所选媒体来源的集中信息。它还可以在汽车熄火-重启后恢复之前正在播放的媒体来源。
  • car-media-common 提供列出、连接媒体应用和与媒体应用互动的便捷方法。

下面提供了常见语音互动命令的具体实现准则。

获取已安装媒体来源的列表

可以使用 PackageManager 检测媒体来源,并过滤与 MediaBrowserService.SERVICE_INTERFACE 匹配的服务。在某些汽车中,可能需要排除一些特殊的媒体浏览器服务实现。下面是此逻辑的示例:

private Map<String, MediaSource> getAvailableMediaSources() {
    List<String> customMediaServices =
        Arrays.asList(mContext.getResources()
            .getStringArray(R.array.custom_media_packages));
    List<ResolveInfo> mediaServices = mPackageManager.queryIntentServices(
            new Intent(MediaBrowserService.SERVICE_INTERFACE),
            PackageManager.GET_RESOLVED_FILTER);
    Map<String, MediaSource> result = new HashMap<>();
    for (ResolveInfo info : mediaServices) {
        String packageName = info.serviceInfo.packageName;
        if (customMediaServices.contains(packageName)) {
            // Custom media sources should be ignored, as they might have a
            // specialized handling (e.g.: radio).
            continue;
        }
        String className = info.serviceInfo.name;
        ComponentName componentName = new ComponentName(packageName,
            className);
        MediaSource source = MediaSource.create(mContext, componentName);
        result.put(source.getDisplayName().toString().toLowerCase(),
            source);
    }
    return result;
}

请注意,媒体来源可以随时安装或卸载。为了保持该列表的准确性,建议为以下 intent 操作实现 BroadcastReceiverACTION_PACKAGE_ADDACTION_PACKAGE_CHANGEDACTION_PACKAGE_REPLACEDACTION_PACKAGE_REMOVED

连接到当前正在播放的媒体来源

CarMediaService 提供了一些方法,可用于获取当前选定的媒体来源,以及该媒体来源切换的时机。之所以发生切换,可能是因为用户直接与界面互动,或者使用了车上的硬件按钮。另一方面,car-media-common 库提供了连接到指定媒体源的便捷方法。以下简化代码段展示了如何连接到当前选定的媒体应用:

public class MediaActuator implements
        MediaBrowserConnector.onConnectedBrowserChanged {
    private final Car mCar;
    private CarMediaManager mCarMediaManager;
    private MediaBrowserConnector mBrowserConnector;

    …

    public void initialize(Context context) {
        mCar = Car.createCar(context);
        mBrowserConnector = new MediaBrowserConnector(context, this);
        mCarMediaManager = (CarMediaManager)
            mCar.getCarManager(Car.CAR_MEDIA_SERVICE);
        mBrowserConnector.connectTo(mCarMediaManager.getMediaSource());
        …
    }

    @Override
    public void onConnectedBrowserChanged(
            @Nullable MediaBrowserCompat browser) {
        // TODO: Handle connected/disconnected browser
    }

    …
}

控制当前正在播放的媒体来源的播放

借助已连接的 MediaBrowserCompat,可以非常轻松地向目标应用发送“传输控制”命令。下面是一个简化示例:

public class MediaActuator …  {
    …
    private MediaControllerCompat mMediaController;

    @Override
    public void onConnectedBrowserChanged(
            @Nullable MediaBrowserCompat browser) {
        if (browser != null && browser.isConnected()) {
            mMediaController = new MediaControllerCompat(mContext,
                browser.getSessionToken());
        } else {
            mMediaController = null;
        }
    }

    private boolean playSongOnCurrentSource(String song) {
        if (mMediaController == null) {
            // No source selected.
            return false;
        }
        MediaControllerCompat.TransportControls controls =
            mMediaController.getTransportControls();
        PlaybackStateCompat state = controller.getPlaybackState();
        if (state == null || ((state.getActions() &
                PlaybackStateCompat.ACTION_PLAY_FROM_SEARCH) == 0)) {
            // Source can't play from search
            return false;
        }
        controls.playFromSearch(query, null);
        return true;
    }

    …
}

处理本地媒体来源命令(电台、CD 播放器、蓝牙、USB)

本地媒体来源使用上文详述的 MediaSession API 和 MediaBrowse API 向系统公开其功能。为了适应每种硬件的特殊性,这些 MediaBrowse 服务使用特定的惯例来组织其信息和媒体命令。

处理电台

电台 MediaBrowseService 可通过 ACTION_PLAY_BROADCASTRADIO intent 过滤器进行标识。它们应遵循用媒体实现电台功能中所述的播放控件和媒体浏览结构。AAOS 提供了 car-broadcastradio-support 库,其中包含一些常量和方法,可帮助原始设备制造商 (OEM) 为其遵循已定义协议的电台服务创建 MediaBrowseService 实现。此外,AAOS 还为使用其浏览树的应用(例如,VIA)提供支持。

处理辅助输入、CD 音频和 USB 媒体

AOSP 中没有这些媒体来源的默认实现。建议的方法如下:

处理蓝牙

蓝牙媒体内容通过 AVRCP 蓝牙配置文件公开。为了轻松访问此功能,AAOS 包含可将通信详细信息抽象化的 MediaBrowserService 和 MediaSession 实现(请参阅 packages/apps/Bluetooth)。

相应的媒体浏览器树结构在 BrowseTree 类中定义。播放控制命令通过使用其 MediaSession 实现,可以通过类似的方式传递到任何其他应用。

处理在线播放媒体命令

如需实现服务器端媒体在线播放,VIA 必须成为媒体来源,并实现 MediaBrowse 和 MediaSession API。请参阅构建车载媒体应用。通过实现这些 API,语音控制应用能够执行以下操作(且不限于以下操作):

  • 无缝参与媒体来源选择
  • 汽车重启后自动恢复
  • 使用媒体中心界面提供播放和浏览控件
  • 接收标准硬件媒体按钮事件

目前还没有与所有导航应用进行互动的标准化方式。如需与 Google 地图集成,请参阅用于 Android Automotive Intent 的 Google 地图。如需与其他应用集成,请直接联系应用开发者。在向任何应用(包括 Google 地图)发布 intent 之前,请先验证能否解析该 intent(请参阅 Intent 请求),以便在目标应用不可用时通知用户。

执行车辆命令

通过 CarPropertyManager 可以提供对车辆属性的读写访问。如需了解车辆属性类型、其实现以及其他详细信息,请参阅 AOSP/开发/汽车 - 车辆属性。有关 Android 支持的属性的准确描述,最好直接参考 hardware/interfaces/automotive/vehicle/2.0/types.hal。其中定义的 VehicleProperty 枚举包含标准属性和供应商特定属性、数据类型、更改模式、单位以及读/写访问定义。

如需从 Java 访问这些常量,您可以使用 VehiclePropertyIds 及其配套类。不同的属性使用不同的 Android 权限来控制其访问。这些权限在 CarService 清单中声明,属性与权限之间的映射在 VehiclePropertyIds Javadoc 中描述,并在 PropertyHalServiceIds 中强制执行。

读取车辆属性

以下示例说明了如何读取车辆速度。

public class CarActuator ... {
    private final Car mCar;
    private final CarPropertyManager mCarPropertyManager;
    private final TextToSpeech mTTS;

    /** Global VHAL area id */
    public static final int GLOBAL_AREA_ID = 0;

    public CarActuator(Context context, TextToSpeech tts) {
        mCar = Car.createCar(context);
        mCarPropertyManager = (CarPropertyManager) mCar.getCarManager(Car.PROPERTY_SERVICE);
        mTTS = tts;
        ...
    }

    @Nullable
    private void getSpeedInMetersPerSecond() {
        if (!mCarPropertyManager.isPropertyAvailable(VehiclePropertyIds.PERF_VEHICLE_SPEED,
                GLOBAL_AREA_ID)) {
            mTTS.speak("I'm sorry, but I can't read the speed of this vehicle");
            return;
        }
        // Data type and unit can be found in
        // automotive/vehicle/2.0/types.hal
        float speedInMps = mCarPropertyManager.getFloatProperty(
                VehiclePropertyIds.PERF_VEHICLE_SPEED, GLOBAL_AREA_ID);
        int speedInMph = (int)(speedInMetersPerSecond * 2.23694f);
        mTTS.speak(String.format("Sure. Your current speed is %d miles "
                + "per hour", speedInUserUnit);
    }

    ...
}

设置车辆属性

以下示例说明如何打开和关闭前置摄像头。

public class CarActuator … {
    …

    private void changeFrontAC(boolean turnOn) {
        List<CarPropertyConfig> configs = mCarPropertyManager
                .getPropertyList(new ArraySet<>(Arrays.asList(
                    VehiclePropertyIds.HVAC_AC_ON)));
        if (configs == null || configs.size() != 1) {
            mTTS.speak("I'm sorry, but I can't control the AC of your vehicle");
            return;
        }

        // Find the front area Ids for the AC property.
        int[] areaIds = configs.get(0).getAreaIds();
        List<Integer> areasToChange = new ArrayList<>();
        for (int areaId : areaIds) {
            if ((areaId & (VehicleAreaSeat.SEAT_ROW_1_CENTER
                        | VehicleAreaSeat.SEAT_ROW_1_LEFT
                        | VehicleAreaSeat.SEAT_ROW_1_RIGHT)) == 0) {
                continue;
            }
            boolean isACInAreaAlreadyOn = mCarPropertyManager
                    .getBooleanProperty(VehiclePropertyIds.HVAC_AC_ON, areaId);
            if ((!isACInAreaAlreadyOn && turnOn) || (isACInAreaAlreadyOn && !turnOn)) {
                areasToChange.add(areaId);
            }
        }
        if (areasToChange.isEmpty()) {
            mTTS.speak(String.format("The AC is already %s", turnOn ? "on" : "off"));
            return;
        }

        for (int areaId : areasToChange) {
            mCarPropertyManager.setBooleanProperty(
                VehiclePropertyIds.HVAC_AC_ON, areaId, turnOn);
        }
        mTTS.speak(String.format("Okay, I'm turning your front AC %s",
            turnOn ? "on" : "off"));
    }

    …
}

执行通信命令

处理消息传递命令

VIA 必须按照语音助理点读功能中所述的“点读”流程来处理收到的消息,该功能可以选择将回复发回给该消息的发送者。此外,VIA 还可以使用 SmsManagerandroid.telephony 软件包的一部分)直接在车上或通过蓝牙编写和发送短信。

处理通话命令

同样,VIA 可以使用 TelephonyManager 拨打电话以及拨打用户的语音信箱号码。在这些情况下,VIA 将直接与电话堆栈互动,或与车载拨号器应用互动。无论在什么情况下,车载拨号器应用都应向用户显示语音通话相关界面。

执行其他命令

有关 VIA 与系统之间其他可能的集成点的列表,请查看众所周知的 Android Intent 列表。许多用户命令可以在服务器端解析(例如,读取用户电子邮件和日历活动),并且除了语音互动本身之外,无需与系统进行任何其他互动。

沉浸式操作(显示视觉内容)

VIA 可以在汽车屏幕上提供辅助性视觉内容,帮助用户改善操作、加强理解。为了尽量避免司机分散注意力,这类内容应简单明了、切实可行。如需详细了解有关沉浸式操作的界面/用户体验指南,请参阅预加载的助理:用户体验指南

为了实现自定义并与其余车机 (HU) 设计保持一致,VIA 应对大多数界面元素使用车载设备界面库组件。如需了解详情,请参阅自定义