Create a Custom Connection Service

Last edit: 2023.07.27

The TomTom Digital Cockpit platform allows end-users to make phone calls using a Bluetooth-connected phone using the Bluetooth Hands Free Profile. TomTom Digital Cockpit can be extended to support other communications services such as VOIP (Voice Over Internet Protocol) calling or integrate an SDK of an online conference calling or messaging service and make and receive calls using the existing communications framework for these new services.

A ConnectionService is a service used by Android to implement calling functionality, like phone calling or any VOIP calling service like WhatsApp. Another communications service can be supported by implementing a custom ConnectionService.

How to create a custom connection service

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

Example code illustrating how to implement the concepts described in this document can be found in examples/telephony/customconnection.

Service module setup

To create a custom connection service, add an AndroidManifest.xml file to your module. In this file your custom service has to be declared so it can be seen by the system. This XML file should also contain the package name together with some permissions:

Add a src/main/AndroidManifest.xml file:

1<manifest xmlns:android="http://schemas.android.com/apk/res/android"
2package="com.example.ivi.example.telephony.customconnection">
3
4<!-- This is needed to place and receive calls using Android's TelecomManager. -->
5<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
6<!-- This is needed to read available phone accounts using Android's TelecomManager. -->
7<uses-permission android:name="android.permission.READ_PHONE_STATE" />
8
9<application>
10 <!-- Declaration of the custom connection service which can be bound with the TelecomManager. -->
11 <service
12 android:name=".CustomConnectionService"
13 android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
14 <intent-filter>
15 <action android:name="android.telecom.ConnectionService" />
16 </intent-filter>
17 </service>
18</application>
19</manifest>

Add a dependency to the TelecomService and LifecycleService in your build.gradle.kts file:

1dependencies {
2 implementation(libraries.iviPlatformTelecomApiServiceTelecom)
3 implementation("androidx.lifecycle:lifecycle-service:2.3.1")
4}

The dependency to the TelecomService is necessary in order to use CallState and toPhoneUri (package com.tomtom.ivi.platform.telecom.api.common.utils), both required for the phone call state management.

The dependency to the LifecycleService is necessary in order to create a dispatcher in your custom service class, as it is not possible to inherit from LifecycleService directly:

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionService.kt

private val dispatcher = ServiceLifecycleDispatcher(this)

Service preparation

To use your custom implementation you need to create an internal object CustomConnectionServiceHolder.kt that will create and store an instance of CustomConnectionService.

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionServiceHolder.kt

1internal object CustomConnectionServiceHolder {
2 // The custom connection service used for communications.
3 @Volatile
4 private var customConnectionService: CustomConnectionService? = null
5
6 // It should be called when we create the CustomConnectionService, so the instance of the
7 // CustomConnectionService is initialized.
8 @Synchronized
9 fun setCustomConnectionService(customConnectionService: CustomConnectionService?) {
10 this.customConnectionService = customConnectionService
11 }
12
13 // Checks if the connection service is ready.
14 @Synchronized
15 fun isCustomConnectionServiceReady() = (customConnectionService != null)
16
17 //...
18}

CustomConnectionService will be set when the TelecomManager binds to the CustomConnectionService, which will happen when creating an incoming or outgoing call. If there is no more ongoing call, the TelecomManager unbinds the CustomConnectionService and this shall be null.

Service definition

In order to create a connection service implementation you need to create a class that inherits from the ConnectionService and LifecycleOwner classes. It should implement the required functions from ConnectionService like onCreateOutgoingConnection, onCreateOutgoingConnectionFailed, onCreateIncomingConnection and onCreateIncomingConnectionFailed. It should also implement getLifecycle from LifecycleService.

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionService.kt

1internal class CustomConnectionService : ConnectionService(), LifecycleOwner {
2 //...
3
4 override fun onCreateOutgoingConnection(
5 connectionManagerPhoneAccount: PhoneAccountHandle?,
6 request: ConnectionRequest?
7 ): Connection? {
8 if (uriAlreadyHasAConnection(request?.address)) {
9 tracer.uriAlreadyHasAConnection(request?.address)
10 return null
11 }
12 return OutgoingCustomConnection(request).apply {
13 setInitializing()
14 setDialing()
15 tracer.onCreateOutgoingConnection(this)
16 }
17 }
18
19 override fun onCreateOutgoingConnectionFailed(
20 connectionManagerPhoneAccount: PhoneAccountHandle?,
21 request: ConnectionRequest?
22 ) {
23 super.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request)
24 tracer.onCreateOutgoingConnectionFailed(connectionManagerPhoneAccount, request)
25 }
26
27 override fun onCreateIncomingConnection(
28 connectionManagerPhoneAccount: PhoneAccountHandle?,
29 request: ConnectionRequest?
30
31 ): Connection? {
32 if (uriAlreadyHasAConnection(request?.address)) {
33 tracer.uriAlreadyHasAConnection(request?.address)
34 return null
35 }
36 return IncomingCustomConnection(request).apply {
37 setAddress(request?.address, TelecomManager.PRESENTATION_ALLOWED)
38 setInitializing()
39 setRinging()
40 tracer.onCreateIncomingConnection(this)
41 }
42 }
43
44 override fun onCreateIncomingConnectionFailed(
45 connectionManagerPhoneAccount: PhoneAccountHandle?,
46 request: ConnectionRequest?
47 ) {
48 super.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request)
49 tracer.onCreateIncomingConnectionFailed(connectionManagerPhoneAccount, request)
50 }
51
52 //...
53
54 override fun getLifecycle() = dispatcher.lifecycle
55
56 //...
57}

In this example we have created the CustomConnectionFacade class that simulates incoming and outgoing calls. This class is also able to change the current call's state. Simulated calls can be created after registering a custom phone account in the system.

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionFacade.kt

1class CustomConnectionFacade(private val context: Context) {
2 //...
3
4 // Registers the custom phone account in the system. Needed before calling [createIncomingCall]
5 // or [createOutgoingCall].
6 private fun registerCustomPhoneAccount(): Boolean {
7 //...
8 telecomManager.registerPhoneAccount(customPhoneAccount)
9 //...
10 }
11
12 // Unregisters the custom phone account from the system. The recent calls related to the custom
13 // phone account will be deleted as well.
14 private fun unregisterCustomPhoneAccount(): Boolean {
15 //...
16 CustomConnectionServiceHolder.withCustomConnectionService {
17 // Clean and destroy all pending connections if there are any.
18 it.clearAllConnections()
19 }
20 telecomManager.unregisterPhoneAccount(customPhoneAccount.accountHandle)
21 //...
22 }
23
24 // Creates an incoming call using the [customPhoneAccount].
25 private fun createIncomingCall(phoneNumber: String) {
26 //...
27 val extras = Bundle()
28 val uri = phoneNumber.toPhoneUri()
29 extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri)
30 telecomManager.addNewIncomingCall(customPhoneAccount.accountHandle, extras)
31 tracer.onCreatedIncomingCall(uri)
32 }
33
34 // Creates an outgoing call using the [customPhoneAccount].
35 private fun createOutgoingCall(phoneNumber: String) {
36 //...
37 val extras = Bundle()
38 val uri = phoneNumber.toPhoneUri()
39 extras.putParcelable(
40 TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,
41 customPhoneAccount.accountHandle
42 )
43 //...
44 telecomManager.placeCall(uri, extras)
45 tracer.onCreatedOutgoingCall(phoneNumber.toPhoneUri())
46 }
47
48 // Updates a call state.
49 fun applyCallState(
50 phoneNumber: String,
51 callState: CallState,
52 disconnectCause: DisconnectCause?
53 ) {
54 tracer.applyCallState(phoneNumber, callState, disconnectCause)
55 CustomConnectionServiceHolder.withCustomConnectionService {
56 // Change call's state.
57 it.changeConnectionState(phoneNumber, callState, disconnectCause)
58 }
59 //...
60 }
61
62 //...
63}

Service lifecycle

To manage the initialization and destruction of the service, override the onCreate and onDestroy methods.

When the service is created:

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionService.kt

1override fun onCreate() {
2 dispatcher.onServicePreSuperOnCreate()
3 CustomConnectionServiceHolder.setCustomConnectionService(this)
4 super.onCreate()
5}

The CustomConnectionServiceHolder creates and stores an instance of the CustomConnectionService.

When the service is destroyed:

src/main/kotlin/com/example/ivi/example/telephony/customconnection/CustomConnectionService.kt

1override fun onDestroy() {
2 dispatcher.onServicePreSuperOnDestroy()
3 super.onDestroy()
4 CustomConnectionServiceHolder.setCustomConnectionService(null)
5}

The instance of the CustomConnectionService is null.