IVI Services

Last edit: 2024.02.01

An IVI system is made up of IVI services which can provide a UI or another service with information. TomTom Digital Cockpit services provide a mechanism to encapsulate longer running tasks and business logic for some distinct functionality in the platform. They don't contain a User Interface (UI) of their own and can run in the background for a long time, matching the platform lifetime if required.

TomTom Digital Cockpit system

A TomTom Digital Cockpit based IVI system is deployed as a single Android application that includes all the functionality of the product. The User Interface (UI) of the application is built using TomTom Digital Cockpit frontend plugins, and are created to display something on the screen. They are subsequently destroyed when content disappears from the screen. Frontend plugins UI interaction must run on a single thread (the UI thread), as dictated by the Android platform. Whereas IVI services should contain functionality that does not need to (or should not) run on the UI thread. IVI services are hosted in service hosts and each service host instance will host at least one IVI service implementation. Service hosts may be configured to run in separate processes, therefore IVI service implementations may as well run in separate processes.

Discoverable services

In a deployed TomTom Digital Cockpit product, a discoverable service can have any number of implementations (whereas a non discoverable service can only have one implementation of each service interface). Clients of a discoverable service interface can discover all the implementations. This allows the adding of many implementations of a defined interface. An example of this is a MessagingProviderService that can for example be implemented for SMS but also for any other messaging technology.

Android services

TomTom Digital Cockpit services hosts are built on Android Services, and use the same mechanisms, but hide some of the details of the Android service implementation. For example, you define the interface to a service directly in Kotlin, and to communicate with it there is no need to provide an IBinder, use a Messenger, or define an AIDL interface. In addition, the binding is done automatically, and the service is ready to use when the serviceReady flag has been set.

By default, Android services run in the main thread of the main process, whereas a TomTom Digital Cockpit service by default starts and runs in a different process.

As service hosts are using the Android Binder framework to transfer data, they have the same limitations as an Android bound service, therefore the data types used in a service's interface can only be primitives, or a type that implements the Parcelable interface.

Service host lifecycle

A service host is configured and registered in the build configuration of the IVI product, but only started when a client is using it. A client starts using a service host by calling createApi and when a service host is created, the onCreate callback is called, which starts the service host's lifecycle. The service implementation must initialize all properties of the service interface at startup, and at this point the service can initialize any uninitialized properties and set itself available with the serviceReady property. If the service has deferred initialization, the availability can be changed in the onRequiredPropertiesInitialized callback instead. Note that the service is not available for clients to use until the serviceReady property is set to true. The service host stays alive as long as there is a client using it, that is, as long as any of the lifecyleOwner passed to createApi is active.

How does it work?

The TomTom Digital Cockpit build system generates multiple classes and methods from the service interface definition based on the annotated interfaces. They all get generated in the same package as the service interface, these classes include an abstract base class for the service host implementation, client's API, and the service IPC logic.

The interface to a service should be defined in a separate module, so both the service host implementation and the client can use it.

Example

Define a service with an interface like this:

1@IviService(
2 serviceId = "com.example.ivi.example.service.exampleservice"
3)
4interface ExampleService {
5
6 @IviServiceFun
7 suspend fun myServiceApi(someParam: String)
8
9 val timeStamp: Long
10
11 // A companion object is needed for the generated code (as a minimum
12 // an empty object needs to be supplied).
13 companion object
14}

The interface itself is annotated with @IviService and any API methods for the service should be annotated with @IviServiceFun. Based on this, the build system generates the necessary classes and methods to create an implementation.

An abstract base class is generated for the implementation of the service host to derive from. The format is the "interface name" + Base, in our example ExampleServiceBase.

The API methods can now be implemented, and once all initialization has been done, the service host can set itself to available, and ready to use.

1class StockExampleService(iviServerContext: IviServerContext) :
2 ExampleServiceBase(iviServerContext) {
3
4 override fun onCreate() {
5 super.onCreate()
6 // ...
7
8 // once all initialisation has been done, set the service to ready.
9 serviceReady = true
10 }
11
12 override suspend fun myServiceApi(someParam: String) {
13 // ...
14 }
15}

On the client side, the service API can then be created with a simple call to the generated createApi() method:

1// Create an instance of service's client API.
2private val exampleServiceApi =
3 ExampleService.createApi(this, frontendContext.iviServiceProvider)

All the properties of the service are mirrored in the API instance, as LiveData<> of the original type, so for example a Long in the service:

val timeStamp: Long

becomes a LiveData<Long> in the service interface:

val timeStamp: LiveData<Long>

Calling service methods

The actual methods are mapped to both a synchronous and an asynchronous version. The synchronous method is using coroutines and are prepended with co, the asynchronous method is appended with Async. These methods are exposed in the <Interface>LiveDataApi as follows.

1// Asynchronous
2// When not 'null', 'onResult' is invoked when the service function execution
3// completes with that result or when the execution failed.
4fun exampleMethodAsync(..., onResult = null)
5
6// Suspendable
7// This allows the function to be called from a Kotlin coroutine.
8// The function throws an exception when the execution failed.
9suspend fun coExampleMethod(...): <ReturnType>

Once the service has set itself ready to be used, the client can directly call the APIs. The following example is using a TomTom Digital Cockpit utility method to call the API once the service is available.

1import com.tomtom.ivi.platform.framework.api.ipc.iviservice.queueOrRun
2
3exampleServiceApi.queueOrRun { service ->
4 service.myServiceApiAsync("Test")
5}

Discoverable service

To create a discoverable service, the interface should instead be annotated with the @IviDiscoverableService annotation. In addition to the createApi generated method, a createApis method is also generated that creates all instances for all registered services for this interface.

More information

For more details on how to create a IVI service, see the Create an IVI Service page, and for more details on how to create a frontend, see the Create a Frontend Plugin page. See Configure the Runtime Deployment of the IVI System for information about how to deploy IVI services.