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(RadioSourceId)26 private val mediaService = MediaService.createApi(this, frontendContext.iviServiceProvider)2728 val stationsMediaItem =29 sourceClient.categories.mapToFolderType(EXTRA_RADIO_FOLDER_TYPE_VALUE_STATIONS)30 val bandsMediaItems = sourceClient.categories.map {31 it.filter { item -> EXTRA_RADIO_FOLDER_TYPE_VALUE_BANDS == item.folderTypeOrNull() }32 }3334 init {35 Options.isItemDumpingEnabled = true36 }3738 internal fun selectType(type: IviMediaItem) = sourceClient.browseTo(type)3940 internal fun startRadio(id: String) =41 mediaService.launchActionAsync(PlayMediaIdFromSourceAction(RadioSourceId, id))4243 override fun createInitialFragmentInitializer() =44 IviFragment.Initializer(RadioFragment(), this)4546 companion object {47 private const val EXTRA_RADIO_FOLDER_TYPE = "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE"48 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_STATIONS = 1L49 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_FAVORITES = 2L50 private const val EXTRA_RADIO_FOLDER_TYPE_VALUE_BANDS = 3L5152 private fun LiveData<List<IviMediaItem>>.mapToFolderType(type: Long) =53 map { it.single { item -> type == item.folderTypeOrNull() } }5455 private fun IviMediaItem.folderTypeOrNull(): Long? =56 getLong(EXTRA_RADIO_FOLDER_TYPE)57 }58}
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.appsuite.media.api.common.frontend.viewmodel.RadioPanel6import com.tomtom.ivi.platform.frontend.api.common.frontend.viewmodels.FrontendViewModel7import com.tomtom.tools.android.api.livedata.valueUpToDate89internal class RadioRecyclerViewItem(10 val itemData: IviMediaItem,11 val clickAction: (RadioRecyclerViewItem) -> Unit12)1314internal class RadioViewModel(panel: RadioPanel) : FrontendViewModel<RadioPanel>(panel) {1516 val isLoading = panel.sourceClient.isLoading1718 val contents: LiveData<List<RadioRecyclerViewItem>> = panel.sourceClient.contents.map { list ->19 list.map { item ->20 RadioRecyclerViewItem(21 itemData = panel.policyProvider.itemMappingPolicy(item),22 clickAction = { item.mediaUri?.toString()?.let { panel.startRadio(it) } }23 )24 }25 }2627 val availableBands = panel.bandsMediaItems.map { bands -> bands.map { it.title } }28 val selectedBandIndex = MutableLiveData(-1)29 .also {30 it.observe(this) { index ->31 if (index < 0) return@observe32 val mediaItem = panel.bandsMediaItems.valueUpToDate?.get(index) ?: return@observe33 panel.selectType(mediaItem)34 }35 }3637 fun onBackPressed() = panel.onBackPressed()38 fun onStationsButtonClicked() =39 panel.stationsMediaItem.valueUpToDate?.let { panel.selectType(it) }40}
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
;
additionally, it links the SourceClient
used in the panel to the necessary
Android Context
.
src/main/kotlin/com/example/ivi/example/media/userflowpolicy/ExampleMediaSourceFragment.kt
1import android.content.Context2import com.tomtom.ivi.platform.frontend.api.common.frontend.IviFragment3import com.example.radio.databinding.RadioFragmentBinding45internal class RadioFragment : IviFragment<RadioPanel, RadioViewModel>(RadioViewModel::class) {6 override val viewFactory =7 ViewFactory(RadioFragmentBinding::inflate) { binding ->8 binding.viewModel = viewModel9 }1011 override fun onAttach(context: Context) {12 super.onAttach(context)13 panel.sourceClient.setContext(context)14 }1516 override fun onDetach() {17 panel.sourceClient.setContext(null)18 super.onDetach()19 }20}
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
.