Create a Custom Alexa Handler Service

Last edit: 2024.04.30

Additional requirements exist for using Amazon Alexa in your TomTom Digital Cockpit product.

The TomTom Digital Cockpit platform offers a stock implementation of a VPA Adaptation service for the Alexa Auto VPA, provided by the stock Alexa service. This service interacts with the Alexa engine by exchanging asynchronous JSON-based messages with the Alexa Auto Client Service (AACS) application. More information about the AACS is available in the Alexa Auto Client Service (AACS) documentation page.

These JSON messages are called "AASB" messages in the Alexa Auto documentation, because they are used by the Alexa Auto Services Bridge (AASB) Extension.

The AASB messages are grouped into "topics", each representing a domain that requires a platform implementation. For details about the AASB messages, see the AASB message interfaces and the AASB message definitions pages.

The stock Alexa service of TomTom Digital Cockpit already implements message handlers for many of the topics offered by Alexa Auto, namely:

  • AddressBook
  • Alerts
  • AlexaClient
  • APL (Alexa Presentation Language)
  • AudioInput
  • AudioOutput
  • Authorization
  • CarControl
  • Messaging
  • Navigation
  • Notifications
  • PhoneCallController
  • PlaybackController
  • PropertyManager
  • SpeechRecognizer
  • TemplateRuntime

The responsibility of a message handler is:

  • To parse AASB messages received by AACS and handle them appropriately (for example, to instruct the navigation engine to start navigation when a StartNavigation message is received).
  • To send "Reply" AASB messages to AACS, reporting whether a request from AACS was successfully executed or not.
  • To send "Publish" AASB messages to AACS, for example, to send a request to Alexa or to report an event.

Some topics, though, are device-specific and cannot be implemented as part of the TomTom Digital Cockpit platform. The AlexaHandlerService is a discoverable IVI service interface that can be implemented by an OEM to fulfill one of these purposes:

  • handle topics that are not supported by the TomTom Digital Cockpit platform.
  • extend or replace some of the existing stock TomTom Digital Cockpit platform message handlers. Currently, the only stock TomTom Digital Cockpit platform message handlers that can be extended or replaced with an AlexaHandlerService implementation are the CarControl and the Navigation handlers. For example, you might want to add support for switching the Cabin Light in the vehicle on and off: this is a setting that Alexa Auto supports, but for which the TomTom Digital Cockpit platform's CarControl message handler doesn't have built-in support. You could then add this functionality by implementing an AlexaHandlerService.

Example app

The TomTom Digital Cockpit SDK comes with an example app showing how to include Alexa in your product and add custom Alexa handler services. See the examples/alexa directory.

By default, this application is excluded from the build, because it requires Alexa libraries that are not publicly available. To build the Alexa example app, you should edit the top-level gradle.properties file:

optInToAlexaExamples=true

Alternatively, you can set this Gradle property from the command-line:

./gradlew assemble -PoptInToAlexaExamples=true

Additionally, you will need to register your own Alexa product with Amazon and obtain a set of Alexa IDs, as explained on the Register your Alexa product with Amazon page.

Once you have your own set of Alexa IDs, you can configure the Alexa example app:

Alternatively, you can also configure the Alexa example app by setting the alexaDeviceInfoClientId and alexaDeviceInfoProductId properties in the top-level local.properties file or as Gradle properties. In this case you won't need to set disableAlexaDeviceIdBuildTimeCheck to true.

The example app includes 2 examples of custom Alexa handler services:

How to create a custom Alexa Handler service

The following sections describe how to create a custom Alexa Handler service implementation.

The Digital Cockpit Alexa example app shows two implementations of such a service, a custom car control handler and a custom equalizer control handler. The code snippets show a generic implementation of such a service, but they link to the source code of the CustomCarControlHandler example implementation.

Create a service implementation module

To implement a custom Alexa Handler service, create a new module under examples/alexa (for example examples/alexa/customalexahandler) and add a Gradle build script.

build.gradle.kts

1import com.tomtom.ivi.platform.gradle.api.framework.config.ivi
2
3plugins {
4 `kotlinx-serialization`
5}
6
7ivi {
8 // The AlexaHandlerService API is currently an experimental feature, and has to be explicitly
9 // opted in.
10 optInToExperimentalApis = true
11}
12
13dependencies {
14 implementation(libraries.iviPlatformAlexaApiCommonUtil)
15 implementation(libraries.iviPlatformAlexaApiServiceAlexahandler)
16}

Declare Alexa dependencies in the /build-logic/libraries.versions.toml file:

iviPlatformAlexaApiCommonUtil = { module = "com.tomtom.ivi.platform:platform_alexa_api_common_util", version.ref = "iviPlatform" }
iviPlatformAlexaApiServiceAlexahandler = { module = "com.tomtom.ivi.platform:platform_alexa_api_service_alexahandler", version.ref = "iviPlatform" }

Implement the Alexa Handler service

The Alexa Handler service can be implemented by defining a class (for example, CustomAlexaHandlerService) that inherits from the abstract AlexaHandlerServiceBase base class and implements the methods defined in the AlexaHandlerService interface.

The service implementation needs to set a number of properties for configuring itself with the TomTom Digital Cockpit platform. Please refer to the AlexaHandlerService API reference documentation for detailed information on these properties.

The properties can be set by overriding the onCreate() method. For example:

CustomAlexaHandlerService.kt

1import com.amazon.aacsconstants.Topic
2import com.tomtom.ivi.platform.alexa.api.service.alexahandler.AlexaHandlerService
3
4override fun onCreate() {
5 super.onCreate()
6
7 topic = Topic.CAR_CONTROL
8 priority = AlexaHandlerService.DEFAULT_HANDLER_PRIORITY
9 aacsConfiguration =
10 """
11 {
12 "aacs.carControl" : {
13 "endpoints": [
14 ...
15 ]
16 }
17 }
18 """
19
20 serviceReady = true
21}

Additionally, the service needs to override a number of methods. Please refer to the AlexaHandlerService API reference documentation for detailed information on these methods.

Parsing AASB messages

The Alexa Handler service is able to communicate with the Alexa Auto Client Service by exchanging JSON based Alexa Auto Services Bridge (AASB) messages. More information on the format of the AASB messages is available in the Alexa Auto AASB message definitions page.

Whenever the TomTom Digital Cockpit platform receives a JSON AASB message from the Alexa Auto Client Service which matches with the topic configured in the service implementation, the AlexaHandlerService.onMessageReceived method is called.

The service implementation needs to parse this message and handle it as needed. Typically, in order to parse an AASB message, you would:

  • Define a data class that represents the message payload to be parsed. For example, the data class representing the CarControl SetControllerValue message would look like this:
    CustomAlexaHandlerService.kt

    1@Serializable
    2data class SetControllerValueIncomingMessagePayload(
    3 val controllerType: String,
    4 val endpointId: String,
    5 val turnOn: Boolean
    6)
  • Decode the AASB message into an instance of the data class using the parseJsonMessage function. For example:
    CustomAlexaHandlerService.kt

    1override suspend fun onMessageReceived(message: AacsMessage): Boolean =
    2 when (message.action) {
    3 Action.CarControl.SET_CONTROLLER_VALUE -> handleSetControllerValue(message.payload)
    4 else -> false
    5 }
    6
    7private fun handleSetControllerValue(message: String): Boolean {
    8 val parsedMessage = parseJsonMessage<SetControllerValueIncomingMessagePayload>(message)
    9 return true
    10}

Sending AASB "Reply" messages

In most cases, you will need to send a reply message to AACS to inform Alexa about whether the request was successfully executed or not. Typically, in order to send an AASB reply message, you would:

  • Define a data class that represents the message payload to be sent to AACS. For example, the data class representing the CarControl SetControllerValueMessageReply message would look like this:
    CustomAlexaHandlerService.kt

    1@Serializable
    2data class SetControllerValueOutgoingMessagePayload(
    3 val success: Boolean
    4)
  • Create an instance of this class and send it using the AacsSenderWrapper helper class. For example:
    CustomAlexaHandlerService.kt

    1private fun sendSetControllerValueReply(messageId: String, success: Boolean) {
    2 val aacsSender = AacsSenderWrapper(iviServiceHostContext)
    3 aacsSender.sendReplyMessage(
    4 messageId,
    5 Topic.CAR_CONTROL,
    6 Action.CarControl.SET_CONTROLLER_VALUE,
    7 SetControllerValueOutgoingMessagePayload(success)
    8 )
    9}

Sending AASB "Publish" messages

Some domains require the handler to proactively send a "Publish" message to AACS, for example, to send a request to Alexa or report an event. Typically, in order to send an AASB "Publish" message, you would:

  • Define a data class that represents the message payload to be sent to AACS. For example, the data class representing the DoNotDisturbChanged message would look like this:

    1@Serializable
    2data class DoNotDisturbChangedOutgoingMessagePayload(
    3 val doNotDisturb: Boolean
    4)
  • Create an instance of this class, encode it as a JSON string and send it using the AacsSenderWrapper helper class. For example:

    1private fun sendDoNotDisturbChanged(success: Boolean) {
    2 val aacsSender = AacsSenderWrapper(iviServiceHostContext)
    3 aacsSender.sendMessage(
    4 Topic.DO_NOT_DISTURB,
    5 Action.DoNotDisturb.DO_NOT_DISTURB_CHANGED,
    6 DoNotDisturbChangedOutgoingMessagePayload(value)
    7 )
    8}

Create a service host

Your module will also need to define a service host where the service will be running, as well as provide a service host builder. This can be achieved by creating two classes.

  • A CustomAlexaHandlerServiceHost class:
    CustomAlexaHandlerServiceHost.kt

    1import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviDiscoverableServiceIdProvider
    2import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostBase
    3import com.tomtom.ivi.platform.framework.api.ipc.iviservice.IviServiceHostContext
    4
    5/**
    6 * A [CustomAlexaHandlerService] host server.
    7 */
    8internal class CustomAlexaHandlerServiceHost(
    9 iviServiceHostContext: IviServiceHostContext,
    10 iviDiscoverableServiceIdProvider: IviDiscoverableServiceIdProvider
    11) :
    12 IviServiceHostBase(iviServiceHostContext) {
    13
    14 override val iviServices = setOf(
    15 CustomAlexaHandlerService(iviServiceHostContext, iviDiscoverableServiceIdProvider)
    16 )
    17}
  • A CustomAlexaHandlerServiceHostBuilder class:
    CustomAlexaHandlerServiceHostBuilder.kt

    1/**
    2 * A [CustomAlexaHandlerServiceHost] builder used to build a [CustomAlexaHandlerService]
    3 * host.
    4 */
    5class CustomAlexaHandlerServiceHostBuilder : IviServiceHostBuilder() {
    6
    7 override fun build(iviServiceHostContext: IviServiceHostContext): IviServiceHostBase =
    8 CustomAlexaHandlerServiceHost(iviServiceHostContext) {
    9 getDiscoverableServiceId(it)
    10 }
    11
    12 companion object
    13}

Please ensure that the CustomAlexaHandlerServiceHostBuilder class is added to the root of your module hierarchy.

Configure the service host deployment

Define an IVI service host implementation in your gradle file. This can also be defined in a top-level gradle file (for example, iviservicehosts.gradle.kts) so it can be used in a multi-project build, including the tests.

/examples/alexa/iviservicehosts.gradle.kts

1import com.tomtom.ivi.buildsrc.dependencies.ExampleModuleReference
2import com.tomtom.ivi.platform.gradle.api.common.dependencies.IviPlatformModuleReference
3import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceHostConfig
4import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceInterfaceConfig
5
6/**
7 * Defines a configuration for the custom Alexa Handler service.
8 *
9 * The configuration specifies the service host implementation and the list of interfaces
10 * implemented by this service host.
11 */
12val customAlexaHandlerServiceHost by extra {
13 IviServiceHostConfig(
14 serviceHostBuilderName = "CustomAlexaHandlerServiceHostBuilder",
15 implementationModule = ExampleModuleReference("services_customalexahandler"),
16 interfaces = listOf(
17 IviServiceInterfaceConfig(
18 serviceName = "AlexaHandlerService",
19 serviceId = "com.tomtom.ivi.example.service.customalexahandler",
20 serviceApiModule = IviPlatformModuleReference(
21 "platform_alexa_api_service_alexahandler"
22 )
23 )
24 )
25 )
26}

Register the service host build configuration in the main application's build script.

/examples/alexa/app/build.gradle.kts

1import com.tomtom.ivi.platform.gradle.api.common.iviapplication.config.IviServiceHostConfig
2import com.tomtom.ivi.platform.gradle.api.framework.config.ivi
3
4// Define the service host configs as defined in the top-level `iviservicehosts.gradle.kts` file.
5apply(from = rootProject.file("examples/alexa/iviservicehosts.gradle.kts"))
6
7// Use Gradle's extra extensions to obtain the `customAlexaHandlerServiceHost` config as defined in
8// the top-level `iviservicehosts.gradle.kts` file.
9val customAlexaHandlerServiceHost: IviServiceHostConfig by project.extra
10
11ivi {
12 application {
13 enabled = true
14 services {
15 // Add the custom Alexa Handler service host to the application.
16 addHost(customAlexaHandlerServiceHost)
17 }
18 }
19}
20
21// The rest of the build script, dependencies, etc.

References