Fulfilling Commands

Learn how to fulfill these types of commands with voice interaction:

Fulfilling Media Commands

Media related command can be split into three different groups:

  • External media sources (such as Spotify installed in AAOS).
  • Backend media sources (such as music streamed through the VIA).
  • Local media sources (such as car radio).

Handling External Media Source Commands

External media sources are defined as Android applications that support MediaSessionCompat and MediaBrowseCompat APIs (refer to Build media apps for cars on detailed explanation on the usage of these APIs).

Important. For an assistant application to connect to the MediaBrowseService of all installed media applications in the system, it must:

  1. Be installed as system-signed (see Media Application Development guidelines for AAOS and the sample PackageValidator code).
  2. Hold android.permission.MEDIA_CONTENT_CONTROL system-privileged permission (see Grant System-Privileged Permissions).

In addition to MediaBrowserCompat and MediaControllerCompat, AAOS provides the following:

  • CarMediaService provides centralized information on the currently selected media source. This is also used to resume a previously playing media source after car shutdown-restart.
  • car-media-common provides convenient methods to list, connect, and interact with media applications.

Provided below are guidelines specific to the implementation of common voice interaction commands.

Get a List of Installed Media Sources

Media sources can be detected using PackageManager, and filtering for services matching the MediaBrowserService.SERVICE_INTERFACE. In some cars there might be some special media browser service implementations, which should be excluded. Below is an example of this logic:

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;
}

Be aware that media sources might be installed or uninstalled at any time. In order to maintain an accurate list, it is recommended to implement a BroadcastReceiver for the following intent actions: ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REPLACED, ACTION_PACKAGE_REMOVED.

Connect to Currently Playing Media Source

CarMediaService provides methods to get the currently selected media source, and when this media source changes. These changes could happen because the user interacted with the UI directly, or due the use of hardware buttons in the car. On the other hand, car-media-common library offers convenient ways to connect to a given media source. Here is a simplified snippet on how to connect to the currently selected media app:

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
    }

    …
}

Control Playback of Currently Playing Media Source

With a connected MediaBrowserCompat it is very simple to send "transport control" commands to the target application. Here is a simplified example:

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;
    }

    …
}

Handling Local Media Source Commands (Radio, CD-Player, Bluetooth, USB)

Local media sources expose their functionality to the system using the same MediaSession and MediaBrowse APIs detailed above. To accommodate the particularities of each type of hardware, these MediaBrowse services use specific conventions to organize their information and media commands.

Handling Radio

Radio MediaBrowseService can be identified by the ACTION_PLAY_BROADCASTRADIO intent filter. They are expected to follow the playback controls and media browse structure described here: Implementing Radio with Media. AAOS offers the car-broadcastradio-support library containing constants and methods to help OEMs create MediaBrowseService implementations for their own radio services that follow the defined protocol, and provides support for applications consuming their browse tree (for example, VIAs).

Handling Auxiliary Input, CD Audio, and USB Media

There is no default implementation of these media sources as part of AOSP. The suggested approach is to:

  • Have OEMs implement media services for each of them. For details, see Build media apps for cars.
  • These MediaBrowseService implementations would be identified and responded to in the Intent actions defined at General Play intents.
  • These services would expose a browse tree following the guidelines described at Other source types.

Handling Bluetooth

Bluetooth media content is exposed through the AVRCP Bluetooth profile. In order to facilitate access to this functionality, AAOS includes a MediaBrowserService and MediaSession implementation that abstracts out the communication details (see packages/apps/Bluetooth).

The respective media browser tree structure is defined at BrowseTree class. Playback control commands can be delivered similarly to any other application, by using its MediaSession implementation.

Handling Streaming Media Commands

To implement server side media streaming, the VIA must become itself a media source, implementing MediaBrowse and MediaSession API. Refer to Build media apps for cars. By implementing these APIs, a voice control application would be able to (among other things):

  • Participate seamlessly in the media source selection
  • Be automatically resumed after car restart
  • Provide playback and browsing control using Media Center UI
  • Receive standard hardware media button events

There is no standardized way of interacting with all navigation applications. For integrations with Google Maps, see Google Maps for Android Automotive Intents. For integrations with other applications, contact the application developers directly. Before launching an intent to any application (including Google Maps), verify that the intent can be resolved (see Intent requests). This creates the opportunity to inform the user in case the target application is not available.

Fulfilling Vehicle Commands

Access to vehicle properties for both read and write is provided through CarPropertyManager. Vehicle properties types, its implementation and other details are explained here: AOSP/Develop/Automotive - Vehicle Properties. For an accurate description of the properties supported by Android, it is best to refer directly to hardware/interfaces/automotive/vehicle/2.0/types.hal. The VehicleProperty enum defined there contains both standard and vendor specific properties, data types, change mode, units and read/write access definition.

To access these same constants from Java, you can use VehiclePropertyIds and its companion classes. Different properties have different Android permissions controlling their access. These permissions are declared in the CarService manifest, and the mapping between properties and permissions described in the VehiclePropertyIds Javadoc and enforced in PropertyHalServiceIds.

Reading a Vehicle Property

The following is an example showing how to read the vehicle speed.

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);
    }

    ...
}

Setting a Vehicle Property

The following is an example showing how to turn on and off the front 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"));
    }

    …
}

Fulfilling Communication Commands

Handling Messaging Commands

VIAs must handle incoming messages following the "tap-to-read" flow described in Voice Assistant Tap-to-Read, which can optionally handle sending replies back to the incoming message sender. Additionally, VIAs can use SmsManager (part of the android.telephony package) to compose and send SMS messages directly from the car or over Bluetooth.

Handling Call Commands

In a similar way, VIAs can use TelephonyManager to place phone calls and call to the user's voice mail number. In these cases, VIAs will interact with the telephony stack directly or with the Car Dialer application. In any case, the Car Dialer application should be the one displaying voice-call related UI to the user.

Fulfilling Other Commands

For a list of other possible points of integration between the VIA and the system, check the list of well-known Android Intents. Many user commands can be resolved server-side (for example, reading users emails and calendar events) and don't require any interactions with the system other than the voice interaction itself.

Immersive Actions (Displaying Visual Content)

Where it enhances user actions or understanding, a VIA can provide supplementary visual content on the car screen. To minimize driver distraction, keep such content simple, brief, and actionable. For details about UI/UX guidelines on immersive actions, see Preloaded Assistants: UX Guidance.

To enable customization and consistency with the rest of the head unit (HU) design, VIAs should use Car UI Library components for most of the UI elements. For details, see Customization.