Customize a Media Source

Most media apps offer content that can be browsed and played freely, leaving the user more or less in control of the playback. Potentially, these apps require logging in before allowing access. This kind of media app is easier to customize, as the TomTom Digital Cockpit Application Suite already provides a delightful user interface for media.

These customization capabilities are currently offered:

  • Modify how content provided by the media app is displayed to the user. See the content display section for details.
  • Add new custom action icons specific to an app. See the custom action section for details.
  • Select which app name, icon and colors are displayed to the user in different contexts. See the name and icon section for details.
  • Define how media items provided by the media app are compared with each other. See the documentation of type MediaItemComparisonPolicy in the package com.tomtom.ivi.appsuite.media.api.common.frontend.policies for more information.

These customizations are possible through the use of policies, which are applied based on the media source that is currently being browsed. After creating a new policy with any number of customizations, the policy installation section explains how to apply them to the build.

More venues for customization are planned for the future, but if it is necessary to change the user experience in a more radical way, then it is possible to create a new user interface.

Throughout this customization guide, a practical example will be built to integrate TomTom Digital Cockpit with an internet radio media app. This fictitious app will be called ExampleInternetRadio.

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

If you want to configure the media plugins with a more global configuration, you can follow this guide: How to configure the media plugins.

Modifying content display

The concepts presented here are implemented in applications that you can find in examples/media/custompolicies and examples/media/miniplayer.

A media source might provide poor quality of its input data, due to a sub-par implementation of the Android Automotive Media API, causing the default user interface to display it incorrectly.

When browsing through content from the ExampleInternetRadio app, normally the TomTom Digital Cockpit user interface would display the IviMediaItem.title field of a playable media item in a more visually-prominent fashion (in bold and with a bigger font size) and the IviMediaItem.subtitle field, when present, in a less-prominent fashion (not in bold, and with a smaller font size).

When browsing stations from ExampleInternetRadio, a playable radio station media item has: its IviMediaItem.title field set to the name of the station; its IviMediaItem.subtitle field set to the full current track name, "Artist - Track Name"; and its IviMediaItem.artist field is left empty.

With such data, the track name will always be shown smaller, and only the radio station's name will always be at the center of the driver's attention. The wish in this example is to invert the title and the subtitle so that the current track name is displayed more prominently, and the radio station's name is shown in a less prominent way.

The first step to achieve this result is to create a new module made to contain all code related to ExampleInternetRadio. In this new module, a new MediaItemMappingPolicy object (see package com.tomtom.ivi.appsuite.media.api.common.frontend.policies), should be created:

src/main/kotlin/com/example/ivi/example/media/custompolicies/ExampleMediaSourceItemMappingPolicy.kt

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem
4import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItemBuilder
5import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaItemMappingPolicy
6
7class ExampleInternetRadioMediaItemMappingPolicy : MediaItemMappingPolicy {
8 override fun invoke(item: IviMediaItem): IviMediaItem {
9 return IviMediaItemBuilder(item)
10 .withTitle(item.subtitle ?: "") // The subtitle is set to the title field.
11 .withDisplaySubtitle(item.title) // The title is set to the subtitle field.
12 .build()
13 }
14}

A MediaItemMappingPolicy customizes how the user interface will display media item data, and can be as complex as necessary: it might be useful to examine whether an item is playable or browsable (or both) before making a decision on how to map the IviMediaItem fields; or maybe the IviMediaItem.id should be parsed to find out what type of content is being played. It all depends on how the media source presents its information. Also note that this data might change over time, posing integration issues when new versions are released potentially with changes in media item data provisioning.

The new ExampleInternetRadioMediaItemMappingPolicy data mapping policy class needs to be specified in a PolicyProvider, which will only be used when browsing ExampleInternetRadio content. The policy installation section explains how.

Add a custom action

Media sources in the Android Automotive Media framework are allowed to provide custom actions for users to perform source-specific tasks, such as saving a track in the user's "Liked tracks" playlist.

The ExampleInternetRadio media source contains a feature to save favorite radio stations to the home page. When playing a radio station, the user should be able to press a button with a heart icon, and while browsing, that station will always be shown on the home page for quick access. When playing a station, it is always possible to remove it from the quick access list by pressing the heart icon again.

During playback, a media source will constantly advertise which custom actions are currently available for the user to perform. The MediaService service API contains an availableActions field, which is always updated with a list of whatever actions the media source provides for the media that is currently being played, activeMediaItem.

Such list of Actions will have to be examined to find out what Actions are available. When ExampleInternetRadio is playing a track, the device's logcat will show events from the stock MediaService called onAvailableActionsChanged, such as this one:

MediaServiceBase: event=onAvailableActionsChanged([PauseAction(id=ivi_media:pause), SkipMediaItemForwardAction(id=ivi_media:skip_media_item_forward), Action(id=heart_this_station)])

When playing a radio station which was already saved by pressing the heart button, the onAvailableActionsChanged event will look something like this:

MediaServiceBase: event=onAvailableActionsChanged([PauseAction(id=ivi_media:pause), SkipMediaItemForwardAction(id=ivi_media:skip_media_item_forward), Action(id=un-heart_this_station)])

The ID un-heart_this_station from the last Action(id=un-heart_this_station) is then needed to remove the favorite state from the current station.

A new ActionMediaControl is needed. Since this seems to be a simple toggle behavior, the ToggleActionMediaControl specialization fits the use case better:

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.core.actions.Action
4import com.tomtom.ivi.appsuite.media.api.common.frontend.controls.ToggleActionMediaControl
5
6class HeartActionMediaControl(context: MediaControlContext) :
7 ToggleActionMediaControl(context) {
8
9 override val drawable: LiveData<DrawableResolver> =
10 MutableLiveData(ResourceDrawableResolver(R.drawable.example_internet_radio_heart_icon))
11
12 override val activateAction = Action("heart_this_station")
13
14 override val activatedAction = Action("un-heart_this_station")
15
16 object Factory : MediaControlFactory {
17 override fun createControlFor(mediaControlContext: MediaControlContext) =
18 HeartActionMediaControl(mediaControlContext)
19 }
20}

Note that in this example the new media control is a custom media control. All media controls which use StandardActionId IDs are considered standard, all others are considered custom media controls. The documentation for MediaControlPolicy offers more information about how to work with standard media controls.

The new HeartActionMediaControl should then be specified in a MediaControlPolicy.

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaControlPolicy
4
5// Note that this class also exposes `replacedStandardControls`, an `override`-able map of
6// `StandardMediaControls` used to replace standard media controls such as play/pause.
7class ExampleInternetRadioMediaControlPolicy : MediaControlPolicy {
8
9 override val customControls = listOf(HeartActionMediaControl.Factory)
10}

Finally, the new ExampleInternetRadioMediaControlPolicy needs to be specified in a PolicyProvider which will only be used when browsing ExampleInternetRadio content. The policy installation section explains how.

Customize name and icon

Branding is a very important aspect of customization. The media app's logo usually needs to be presented in a way consistent with branding guidelines, available space for the logo, and the current theme's colors.

The company behind the ExampleInternetRadio app may have the following branding guidelines: The name must be fully displayed as "Example Internet Radio", if possible, and only if not possible (for example, due to insufficient space in the UI), the name must then be displayed as "ExampleRadio". Different icons and colors are also prescribed for light and dark themes.

To aid the process of adherence to these branding guidelines, the MediaSourceAttributionPolicy policy is available. A SourceAttributionFormat parameter can be used to specify the preferred UI display mode depending on the current display context. The mediaItem parameter can be used to determine what to display based on the metadata contained in the current media item.

src/main/kotlin/com/example/ivi/example/media/custompolicies/ExampleMediaSourceSourceAttributionPolicy.kt

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem
4import com.tomtom.ivi.appsuite.media.api.common.core.SourceInfo
5import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaSourceAttributionPolicy
6import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.SourceAttributionFormat
7import com.tomtom.tools.android.api.resourceresolution.drawable.DrawableResolver
8import com.tomtom.tools.android.api.resourceresolution.drawable.ResourceDrawableResolver
9import com.tomtom.tools.android.api.resourceresolution.string.ResourceStringResolver
10import com.tomtom.tools.android.api.resourceresolution.string.StringResolver
11
12class ExampleInternetRadioSourceAttributionPolicy : MediaSourceAttributionPolicy {
13 override fun getName(
14 sourceInfo: SourceInfo?,
15 mediaItem: IviMediaItem?,
16 sourceAttributionFormat: SourceAttributionFormat
17 ): StringResolver? {
18 return if (sourceAttributionFormat.preferSimplified)
19 ResourceStringResolver(R.string.simple_name)
20 else
21 ResourceStringResolver(R.string.full_name)
22 }
23
24 override fun getLogo(
25 sourceInfo: SourceInfo?,
26 mediaItem: IviMediaItem?,
27 sourceAttributionFormat: SourceAttributionFormat
28 ): DrawableResolver {
29 return if (sourceAttributionFormat.preferWordMark)
30 ResourceDrawableResolver(R.drawable.ic_sel_logo_with_word_mark)
31 else
32 ResourceDrawableResolver(R.drawable.ic_sel_logo_only)
33 }
34
35 override fun getStyle(): SourceStyle {
36 return SourceStyle(AttrColorResolver(R.attr.exampleinternetradio_accent_color))
37 }
38}

The getName() method uses the right string at the right time, and the getLogo() method uses resources which are selected by Android by the type of theme currently in use. The getStyle() method can return a SourceStyle to override the default accentColor provided by the source APK theme. When not specified, it returns null and will keep the default color.

The new ExampleInternetRadioSourceAttributionPolicy needs to be specified in a PolicyProvider which will be used only when browsing ExampleInternetRadio content. Section policy installation explains how to do this.

Install customization policies

Policy configuration is done using a MediaPolicyFrontendExtension. When the user starts browsing the internet radio, this configuration will be used to customize the user experience:

src/main/kotlin/com/example/ivi/example/media/custompolicies/ExampleMediaSourceFrontendExtension.kt

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaPolicyFrontendExtension
4import com.tomtom.ivi.platform.frontend.api.common.frontend.FrontendExtension
5
6val exampleInternetRadioFrontendExtension: FrontendExtension =
7 MediaPolicyFrontendExtension(
8 // This needs to match the Android package name of the media app.
9 sourcePackageName = "com.example.exampleinternetradio",
10 // The class name is optional and only required if the policy must explicitly target a
11 // specific MediaBrowserService in the package, such as when the app contains more than one
12 // such service. This is needed to apply a different policy per service within the same
13 // package.
14 sourceClassName = "com.example.MediaService",
15 policyProvider = exampleInternetRadioPolicyProvider
16 )

This is an example defining how exampleInternetRadioPolicyProvider might look with all the customizations described in the other sections.

src/main/kotlin/com/example/ivi/example/media/custompolicies/ExampleMediaSourcePolicyProvider.kt

1package com.example.exampleinternetradio
2
3import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.PolicyProvider
4
5val exampleInternetRadioPolicyProvider = PolicyProvider(
6 itemMappingPolicy = ExampleInternetRadioMediaItemMappingPolicy(),
7 sourceAttributionPolicy = ExampleInternetRadioSourceAttributionPolicy(),
8 mediaControlPolicy = ExampleInternetRadioMediaControlPolicy()
9)

Activating the new exampleInternetRadioFrontendExtension is done via a Gradle configuration. The guide on How to create a frontend plugin and the documentation for FrontendExtension are available for more details.