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 packagecom.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`](https://developer.tomtom.com/assets/downloads/tomtom-digital-cockpit/platform-api/1.0.7805/appsuite_media_api_common_core/com.tomtom.ivi.appsuite.media.api.common.core/-ivi-media-item/subtitle.html) 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.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem4import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItemBuilder5import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaItemMappingPolicy67class 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 Action
s 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.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.core.actions.Action4import com.tomtom.ivi.appsuite.media.api.common.frontend.controls.ToggleActionMediaControl56class HeartActionMediaControl(context: MediaControlContext) :7 ToggleActionMediaControl(context) {89 override val drawable: LiveData<DrawableResolver> =10 MutableLiveData(ResourceDrawableResolver(R.drawable.example_internet_radio_heart_icon))1112 override val activateAction = Action("heart_this_station")1314 override val activatedAction = Action("un-heart_this_station")1516 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.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaControlPolicy45// Note that this class also exposes `replacedStandardControls`, an `override`-able map of6// `StandardMediaControls` used to replace standard media controls such as play/pause.7class ExampleInternetRadioMediaControlPolicy : MediaControlPolicy {89 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.
1package com.example.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.core.IviMediaItem4import com.tomtom.ivi.appsuite.media.api.common.core.SourceInfo5import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.MediaSourceAttributionPolicy6import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.SourceAttributionFormat7import com.tomtom.tools.android.api.resourceresolution.drawable.DrawableResolver8import com.tomtom.tools.android.api.resourceresolution.drawable.ResourceDrawableResolver9import com.tomtom.tools.android.api.resourceresolution.string.ResourceStringResolver10import com.tomtom.tools.android.api.resourceresolution.string.StringResolver1112class ExampleInternetRadioSourceAttributionPolicy : MediaSourceAttributionPolicy {13 override fun getName(14 sourceInfo: SourceInfo?,15 mediaItem: IviMediaItem?,16 sourceAttributionFormat: SourceAttributionFormat17 ): StringResolver? {18 return if (sourceAttributionFormat.preferSimplified)19 ResourceStringResolver(R.string.simple_name)20 else21 ResourceStringResolver(R.string.full_name)22 }2324 override fun getLogo(25 sourceInfo: SourceInfo?,26 mediaItem: IviMediaItem?,27 sourceAttributionFormat: SourceAttributionFormat28 ): DrawableResolver {29 return if (sourceAttributionFormat.preferWordMark)30 ResourceDrawableResolver(R.drawable.ic_sel_logo_with_word_mark)31 else32 ResourceDrawableResolver(R.drawable.ic_sel_logo_only)33 }3435 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.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.frontend.MediaPolicyFrontendExtension4import com.tomtom.ivi.platform.frontend.api.common.frontend.FrontendExtension56val 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 a11 // specific MediaBrowserService in the package, such as when the app contains more than one12 // such service. This is needed to apply a different policy per service within the same13 // package.14 sourceClassName = "com.example.MediaService",15 policyProvider = exampleInternetRadioPolicyProvider16 )
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.exampleinternetradio23import com.tomtom.ivi.appsuite.media.api.common.frontend.policies.PolicyProvider45val 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.