Create a New Media User Interface

Last edit: 2023.07.27

The TomTom Digital Cockpit Application Suite provides a default user interface (UI). This user interface might not be suitable for a media app (also called "media source"), when it does not conform to the typical organization of content, or when their offered content can not be liberally browsed or played or controlled. Such sources would look disappointing or completely unusable in the default user interface from the TomTom Digital Cockpit Application Suite.

A few examples of media apps which are not suitable for the default user interface:

  • A controller/player for an AM/FM/DAB+ radio tuner installed in the system: The default user interface would show the bands, stations, and favorites lumped together as tabs, and dump together all known stations in a single tab.
  • An app which can only stream music (for example via Bluetooth, or via Wi-Fi): No content would be available for browsing, without explanation.
  • An app that only displays remotely broadcast video streams: The default user interface is not suitable for video playback, and if the app would only receive broadcast video streams, no content would be available for browsing, with no explanation.

If such drastic customizations are not necessary, for example if the only needs are fixing up how the content from a media app is displayed to the user, or to add an icon to perform an action specific to that app, using the default media user interface should be preferred and appropriately customized.

For this guide, knowledge of the TomTom Digital Cockpit appsuite_media_api_common_core and appsuite_media_api_common_frontend modules will greatly help. In the media overview, more details can be found over the Android Automotive Media framework and how TomTom Digital Cockpit uses it.

Concepts

The guide will implement the user interface for a simple radio made available in the system through the standard Android Automotive Radio API. Through the standard API for media, an Android media browser service will provide access to a hardware radio tuner module.

This user interface will be a new panel. A panel is composed by three classes: a TaskPanel to define the logic; a view model, FrontendViewModel, potentially using data binding; an IviFragment defining the Android fragment and creating the instance of the view model. Please refer to the frontend plugin guide to get started.

This user interface will use a specialization of the base user interface panel type TaskPanel, MediaTaskPanel. This panel type is more suitable for media apps, as it contains media-specific facilities.

This being merely an example, the user interface is very sparse and only contains one panel to display, and no functionality other than basic browsing and playing.

Example panel

An application that implements the concepts presented here is provided in the examples/media/userflowpolicy directory.

The example's panel class, based on MediaTaskPanel, enables browsing through the stations recognized by the radio and the raw frequencies offered by each radio band.

In the panel the media Options are used to print out to the device logcat all media items retrieved by all media services: this helps only in the initial discovery phase to analyze the format of all content returned by the media source. This setting must not be used in production, and will not function in release builds.

The RootSourceClient is used to browse the content and retrieve the available categories (in the case of radio, those are bands and stations), while the MediaService lets the user play a radio.

src/main/kotlin/com/example/ivi/example/media/userflowpolicy/ExampleMediaSourcePanel.kt

1import androidx.lifecycle.LiveData
2import androidx.lifecycle.map
3import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem
4import com.tomtom.ivi.appsuite.media.api.common.core.Options
5import com.tomtom.ivi.appsuite.media.api.common.core.RootSourceClient
6import com.tomtom.ivi.appsuite.media.api.common.core.SourceId
7import com.tomtom.ivi.appsuite.media.api.common.core.actions.standard.PlayMediaIdFromSourceAction
8import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaTaskPanel
9import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaFrontendContext
10import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.PolicyProvider
11import com.tomtom.ivi.appsuite.media.api.service.core.MediaService
12import com.tomtom.ivi.appsuite.media.api.service.core.createApi
13import com.tomtom.ivi.platform.frontend.api.common.frontend.IviFragment
14
15// This is an example source ID, matching the default Android Automotive car radio service.
16internal object RadioSourceId :
17 SourceId("com.android.car.radio", "com.android.car.radio.service.RadioAppService")
18
19internal class RadioPanel(mediaContext: MediaFrontendContext) :
20 MediaTaskPanel(mediaContext, RadioSourceId, null) {
21
22 val policyProvider: PolicyProvider =
23 mediaFrontendContext.mediaConfiguration.getPolicyProvider(RadioSourceId)
24
25 val sourceClient = RootSourceClient(
26 mediaContext.frontendContext.applicationContext,
27 RadioSourceId
28 )
29 private val mediaService = MediaService.createApi(this, frontendContext.iviServiceProvider)
30
31 val stationsMediaItem =
32 sourceClient.categories.mapToFolderType(EXTRA_RADIO_FOLDER_TYPE_VALUE_STATIONS)
33 val bandsMediaItems = sourceClient.categories.map {
34 it.filter { item -> EXTRA_RADIO_FOLDER_TYPE_VALUE_BANDS == item.folderTypeOrNull() }
35 }
36
37 init {
38 Options.isItemDumpingEnabled = true
39 }
40
41 internal fun selectType(type: IviMediaItem) = sourceClient.browseTo(type)
42
43 internal fun startRadio(id: String) =
44 mediaService.launchActionAsync(PlayMediaIdFromSourceAction(RadioSourceId, id))
45
46 override fun createInitialFragmentInitializer() =
47 IviFragment.Initializer(RadioFragment(), this)
48
49 override fun onAddedToFrontend() {
50 super.onAddedToFrontend()
51 sourceClient.connect()
52 }
53
54 override fun onRemovedFromFrontend() {
55 sourceClient.disconnect()
56 super.onRemovedFromFrontend()
57 }
58
59 companion object {
60 private const val EXTRA_RADIO_FOLDER_TYPE = "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE"
61 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_STATIONS = 1L
62 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_FAVORITES = 2L
63 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_BANDS = 3L
64
65 private fun LiveData<List<IviMediaItem>>.mapToFolderType(type: Long) =
66 map { it.single { item -> type == item.folderTypeOrNull() } }
67
68 private fun IviMediaItem.folderTypeOrNull(): Long? =
69 getLong(EXTRA_RADIO_FOLDER_TYPE)
70 }
71}

Example view model

The view model, a FrontendViewModel, transforms the panel's data into information ready to use in a view.

In this example, the RadioRecyclerViewItem type represents an entry to display with a standard Android RecyclerView adapter.

There is a "back" button, which triggers the onBackPressed function to dismiss the panel; and a "Stations list" button, that calls onStationsButtonClicked to show the list of stations recognized by the tuner.

src/main/kotlin/com/example/ivi/example/media/userflowpolicy/ExampleMediaSourceViewModel.kt

1import androidx.lifecycle.LiveData
2import androidx.lifecycle.MutableLiveData
3import androidx.lifecycle.map
4import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem
5import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel
6import com.tomtom.tools.android.api.livedata.valueUpToDate
7
8internal class RadioRecyclerViewItem(
9 val itemData: IviMediaItem,
10 val clickAction: (RadioRecyclerViewItem) -> Unit
11)
12
13internal class RadioViewModel(panel: RadioPanel) : FrontendViewModel<RadioPanel>(panel) {
14
15 val isLoading = panel.sourceClient.isLoading
16
17 val contents: LiveData<List<RadioRecyclerViewItem>> = panel.sourceClient.contents.map { list ->
18 list.map { item ->
19 RadioRecyclerViewItem(
20 itemData = panel.policyProvider.itemMappingPolicy(item),
21 clickAction = { item.mediaUri?.toString()?.let { panel.startRadio(it) } }
22 )
23 }
24 }
25
26 val availableBands = panel.bandsMediaItems.map { bands -> bands.map { it.title } }
27 val selectedBandIndex = MutableLiveData(-1)
28 .also {
29 it.observe(this) { index ->
30 if (index < 0) return@observe
31 val mediaItem = panel.bandsMediaItems.valueUpToDate?.get(index) ?: return@observe
32 panel.selectType(mediaItem)
33 }
34 }
35
36 fun onBackPressed() = panel.onBackPressed()
37 fun onStationsButtonClicked() =
38 panel.stationsMediaItem.valueUpToDate?.let { panel.selectType(it) }
39}

Example fragment

The fragment, inheriting from IviFragment, is mostly a container for glue code to connect the ViewModel to the XML layout, represented by RadioFragmentBinding.

src/main/kotlin/com/example/ivi/example/media/userflowpolicy/ExampleMediaSourceFragment.kt

1import com.tomtom.ivi.platform.frontend.api.common.frontend.IviFragment
2import com.example.radio.databinding.RadioFragmentBinding
3
4internal class RadioFragment : IviFragment<RadioPanel, RadioViewModel>(RadioViewModel::class) {
5 override val viewFactory =
6 ViewFactory(RadioFragmentBinding::inflate) { binding ->
7 binding.viewModel = viewModel
8 }
9}

Media view model components

You can easily create views that display playback information and/or contain media controls by using the media-oriented set of view models: MediaPlaybackViewModel, TouchTrackViewModel, and MediaButtonsViewModel.