Create a New Media User Interface
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.LiveData2import androidx.lifecycle.map3import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem4import com.tomtom.ivi.appsuite.media.api.common.core.Options5import com.tomtom.ivi.appsuite.media.api.common.core.RootSourceClient6import com.tomtom.ivi.appsuite.media.api.common.core.SourceId7import com.tomtom.ivi.appsuite.media.api.common.core.actions.standard.PlayMediaIdFromSourceAction8import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaTaskPanel9import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaFrontendContext10import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.PolicyProvider11import com.tomtom.ivi.appsuite.media.api.service.core.MediaService12import com.tomtom.ivi.appsuite.media.api.service.core.createApi13import com.tomtom.ivi.platform.frontend.api.common.frontend.IviFragment1415// 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")1819internal class RadioPanel(mediaContext: MediaFrontendContext) :20 MediaTaskPanel(mediaContext, RadioSourceId, null) {2122 val policyProvider: PolicyProvider =23 mediaFrontendContext.mediaConfiguration.getPolicyProvider(RadioSourceId)2425 val sourceClient = RootSourceClient(26 mediaContext.frontendContext.applicationContext,27 RadioSourceId28 )29 private val mediaService = MediaService.createApi(this, frontendContext.iviServiceProvider)3031 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 }3637 init {38 Options.isItemDumpingEnabled = true39 }4041 internal fun selectType(type: IviMediaItem) = sourceClient.browseTo(type)4243 internal fun startRadio(id: String) =44 mediaService.launchActionAsync(PlayMediaIdFromSourceAction(RadioSourceId, id))4546 override fun createInitialFragmentInitializer() =47 IviFragment.Initializer(RadioFragment(), this)4849 override fun onAddedToFrontend() {50 super.onAddedToFrontend()51 sourceClient.connect()52 }5354 override fun onRemovedFromFrontend() {55 sourceClient.disconnect()56 super.onRemovedFromFrontend()57 }5859 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 = 1L62 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_FAVORITES = 2L63 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_BANDS = 3L6465 private fun LiveData<List<IviMediaItem>>.mapToFolderType(type: Long) =66 map { it.single { item -> type == item.folderTypeOrNull() } }6768 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.LiveData2import androidx.lifecycle.MutableLiveData3import androidx.lifecycle.map4import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem5import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel6import com.tomtom.tools.android.api.livedata.valueUpToDate78internal class RadioRecyclerViewItem(9 val itemData: IviMediaItem,10 val clickAction: (RadioRecyclerViewItem) -> Unit11)1213internal class RadioViewModel(panel: RadioPanel) : FrontendViewModel<RadioPanel>(panel) {1415 val isLoading = panel.sourceClient.isLoading1617 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 }2526 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@observe31 val mediaItem = panel.bandsMediaItems.valueUpToDate?.get(index) ?: return@observe32 panel.selectType(mediaItem)33 }34 }3536 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.IviFragment2import com.example.radio.databinding.RadioFragmentBinding34internal class RadioFragment : IviFragment<RadioPanel, RadioViewModel>(RadioViewModel::class) {5 override val viewFactory =6 ViewFactory(RadioFragmentBinding::inflate) { binding ->7 binding.viewModel = viewModel8 }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
.