履行命令

了解如何通過語音交互來完成這些類型的命令:

履行媒體命令

媒體相關的命令可以分為三個不同的組:

  • 外部媒體源(例如安裝在 AAOS 中的 Spotify)。
  • 後端媒體源(例如通過 VIA 流式傳輸的音樂)。
  • 當地媒體來源(如汽車收音機)。

處理外部媒體源命令

外部媒體源定義為支持MediaSessionMediaBrowse 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;
}

請注意,可能隨時安裝或卸載媒體源。為了保持準確的列表,建議為以下意圖操作實現BroadcastReceiverACTION_PACKAGE_ADDEDACTION_PACKAGE_CHANGEDACTION_PACKAGE_REPLACEDACTION_PACKAGE_REMOVED

連接到當前播放的媒體源

CarMediaService提供了獲取當前選擇的媒體源以及該媒體源何時發生變化的方法。這些變化可能是因為用戶直接與 UI 交互,或者由於使用了汽車中的硬件按鈕。另一方面,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 和 MediaBrowse API 向系統公開其功能。為了適應每種硬件的特殊性,這些 MediaBrowse 服務使用特定的約定來組織它們的信息和媒體命令。

處理無線電

Radio MediaBrowseService 可以通過ACTION_PLAY_BROADCASTRADIO意圖過濾器來識別。他們應該遵循此處描述的播放控件和媒體瀏覽結構: Implementing Radio with Media 。 AAOS 提供了car-broadcastradio-support庫,其中包含常量和方法,以幫助 OEM 為他們自己的遵循定義協議的無線電服務創建 MediaBrowseService 實現,並為使用其瀏覽樹的應用程序(例如,VIA)提供支持。

處理輔助輸入、CD 音頻和 USB 媒體

作為 AOSP 的一部分,這些媒體源沒有默認實現。建議的方法是:

處理藍牙

藍牙媒體內容通過 AVRCP 藍牙配置文件公開。為了便於訪問此功能,AAOS 包含一個 MediaBrowserService 和 MediaSession 實現,用於抽像出通信細節(參見packages/apps/Bluetooth )。

相應的媒體瀏覽器樹結構在BrowseTree類中定義。通過使用其 MediaSession 實現,可以像任何其他應用程序一樣交付播放控制命令。

處理流媒體命令

要實現服務器端媒體流,VIA 必須自己成為媒體源,實現 MediaBrowse 和 MediaSession API。請參閱為汽車構建媒體應用程序。通過實現這些 API,語音控制應用程序將能夠(除其他外):

  • 無縫參與媒體源選擇
  • 汽車重啟後自動恢復
  • 使用 Media Center UI 提供播放和瀏覽控制
  • 接收標準硬件媒體按鈕事件

沒有與所有導航應用程序交互的標準化方式。有關與 Google Maps 的集成,請參閱適用於 Android Automotive Intents 的 Google Maps 。如需與其他應用程序的集成,請直接聯繫應用程序開發人員。在向任何應用程序(包括 Google 地圖)啟動意圖之前,請驗證是否可以解析該意圖(請參閱意圖請求)。這創造了在目標應用程序不可用的情況下通知用戶的機會。

履行車輛命令

通過CarPropertyManager提供對車輛屬性的讀寫訪問。車輛屬性類型、其實現和其他細節在此處解釋: AOSP/Develop/Automotive - Vehicle Properties 。 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);
    }

    ...
}

設置車輛屬性

以下是顯示如何打開和關閉前置 AC 的示例。

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 必須按照Voice Assistant Tap-to-Read中描述的“點擊閱讀”流程處理傳入消息,它可以選擇處理將回復發送回傳入消息發送者。此外,VIA 可以使用SmsManagerandroid.telephony包的一部分)直接從汽車或通過藍牙編寫和發送 SMS 消息。

處理調用命令

以類似的方式,VIA 可以使用TelephonyManager撥打電話並撥打用戶的語音郵件號碼。在這些情況下,VIA 將直接與電話堆棧或與 Car Dialer 應用程序交互。在任何情況下,Car Dialer 應用程序都應該是向用戶顯示語音呼叫相關 UI 的應用程序。

執行其他命令

有關 VIA 和系統之間其他可能集成點的列表,請查看眾所周知的Android Intents列表。許多用戶命令可以在服務器端解析(例如,閱讀用戶電子郵件和日曆事件),並且除了語音交互本身之外不需要與系統進行任何交互。

沉浸式動作(顯示視覺內容)

在增強用戶操作或理解的情況下,VIA 可以在汽車屏幕上提供補充視覺內容。為盡量減少駕駛員分心,請保持此類內容簡單、簡短且可操作。有關沉浸式操作的 UI/UX 指南的詳細信息,請參閱預加載助手:UX 指南

為了實現與其他主機 (HU) 設計的定制和一致性,VIA 應該為大多數 UI 元素使用汽車 UI 庫組件。有關詳細信息,請參閱自定義