Radio control implementation

Radio control implementation is based on MediaSession and MediaBrowse, which enable Media and voice assistant apps to control the radio. For more information, see Build media apps for cars on developer.android.com.

A media browse tree implementation is provided in the car-broadcastradio-support library in packages/apps/Car/libs. This library also contains extensions of ProgramSelector to convert to and from URI. It is recommended that radio implementations use this library to build the associated browse tree.

Media source switcher

To provide a seamless transition between radio and other apps displayed in media, the car-media-common library contains classes that should be integrated into the radio app. MediaAppSelectorWidgetcan be included in the XML for the radio app (the icon and drop-down used in the reference media and radio apps):

<com.android.car.media.common.MediaAppSelectorWidget
    android:id="@+id/app_switch_container"
    android:layout_width="@dimen/app_switch_widget_width"
    android:layout_height="wrap_content"
    android:background="@drawable/app_item_background"
    android:gravity="center" />

This widget launches the AppSelectionFragment, which displays a list of media sources that can be switched to. If a UI other than that provided is desired, you can create a custom widget to launch the AppSelectionFragment when the switcher should be displayed.

AppSelectionFragment newFragment = AppSelectionFragment.create(widget,
            packageName, fullScreen);
    newFragment.show(mActivity.getSupportFragmentManager(), null);

A sample implementation is provided in the reference radio app implementation, located in packages/apps/Car/Radio.

Detailed control specifications

The MediaSession (through MediaSession.Callback) interface provides control mechanisms for the currently playing radio program:

  • onPlay, onStop. (Un)mute radio playback.
  • onPause. Time-shifted pause (if supported).
  • onPlayFromMediaId. Play any content from a top-level folder. For example, "Play FM" or "Play Radio."
  • onPlayFromUri. Play a specific frequency. For example, "Play 88.5 FM."
  • onSkipToNext, onSkipToPrevious. Tune to a next or previous station.
  • onSetRating. Add or remove to or from Favorites.

The MediaBrowser exposes a tunable MediaItem over three types of top-level directories:

  • (Optional) Programs (stations). This mode is typically used by dual-tuner radios to indicate all available tuneable radio stations at the user's location.
  • Favorites. Radio programs added to the Favorites list, some may be unavailable (out of reception range).
  • Band channels. All physically possible channels in the current region (87.9, 88.1, 88.3, 88.5, 88.7, 88.9, 89.1 and so on). Every band has a separate top-level directory.
MediaBrowserService tree structure
Figure 2. MediaBrowserService tree structure

Each element in each of these folders (AM/FM/Programs) is a MediaItem with a URI that can be used with MediaSession to tune. Each top-level folder (AM/FM/Programs) is a MediaItem with a mediaId that can be used with MediaSession to trigger playback and is up to the discretion of the OEM. For example, "Play FM," "Play AM," and "Play Radio" are all non-specific radio queries that use a mediaId to send to the OEM radio app. It's up to the radio app to determine what to play from the generic request and the mediaId.

MediaSession

Given there is no concept of pausing a broadcast stream, the Play, Pause, and Stop actions do not always apply to radio. With radio, the Stop action is associated with muting the stream while Play is associated with removing the mute.

Some radio tuners (or apps) provide the ability to simulate a broadcast stream pause by caching content and then playing it back later. In such cases, use onPause.

Playing from mediaId and URI actions is intended to tune to a station fetched from the MediaBrowser interface. The mediaId is an arbitrary string provided by the radio app to impose a unique (so a given ID points to only one item) and stable (so a given item has the same ID through the whole session) value with which to identify a given station. The URI will be of a well-defined schema. In short, a URI-ized form of ProgramSelector. While this preserves the uniquity attribute, it need not be stable, although it can change when the station moves to a different frequency.

By design, onPlayFromSearch is not used. Is is the responsibility of the client (companion app) to select a search result from the MediaBrowser tree. Moving that responsibility to the radio app would increase complexity, require formal contracts on how string queries should appear, and result in an uneven user experience on different hardware platforms.

Note: The radio app does not contain additional information that would be useful to search for a station name not exposed to the client through the MediaBrowser interface.

Skipping to the next or previous station depends on the current context:

  • When an app is tuned to a station from the Favorites list, the app can move to the next station from the Favorites list.
  • Listening to a station from the Program list may result in tuning to the next available station, sorted according to channel number.
  • Listening to an arbitrary channel may result in tuning to the next physical channel, even when there is no broadcast signal.

The radio app handles these actions.

Error handling

TransportControls actions (Play, Stop, and Next) doesn't provide feedback as to whether the action succeeds or not. The only way to indicate an error is to set the MediaSession state to STATE_ERROR with an error message.

The radio app must handle those actions and either execute them or set an error state. If executing the Play command is not immediate, the playback state should be changed to STATE_CONNECTING (in case of direct tune) or STATE_SKIPPING_TO_PREVIOUS or NEXT while the command is being executed.

The client should watch the PlaybackState and verify that the session changed the current program to what was requested or entered into the error state. STATE_CONNECTING must not exceed 30s. However, a direct tune to a given AM/FM frequency should perform much faster.

Add and remove favorites

MediaSession has rating support, which can be used to control Favorites. onSetRating called with a rating of type RATING_HEART adds or removes the currently tuned station to or from the Favorites list.

Contrary to legacy presets, this model assumes an unordered and unbounded Favorites list, when each saved favorite was allocated to a numerical slot (typically, 1 to 6). As a result, preset-based systems would be incompatible with onSetRating operation.

The limitation of the MediaSession API is that only the station currently tuned to can be added or removed. For example, items must be selected first before they can be removed. This is only a limitation of the MediaBrowser client, such as a companion app. The radio app is not similarly restricted. This portion is optional when an app doesn't support Favorites.

MediaBrowser

To express which frequencies or physical channel names (when tuning to an arbitrary channel is suitable for a given radio technology) are valid for a given region, all valid channels (frequencies) are listed for each band. In the US region, this amounts to 101 FM channels from in the range of 87.8 to 108.0 MHz range (using 0.2MHz spacing) and 117 AM channels in the range of 530 to 1700 kHz (using 10kHz spacing). Because HD radio uses the same channel space, it is not presented separately.

The list of currently available radio programs is flat in that this doesn't allow display schemes such as grouping by direct audio broadcast (DAB) ensemble.

Entries on the Favorite list may not be tunable. For example if a given program is out of range. The radio app may or may not detect if the entry can be tuned to beforehand. If so, it may not mark the entry as playable.

To identify top-level folders, the same mechanism used by Bluetooth is applied. That is, an Extras bundle of the MediaDescription object contains a tuner-specific field just as Bluetooth does with EXTRA_BT_FOLDER_TYPE. In the case of broadcast radio, this leads to defining the following new fields in the public API:

  • EXTRA_BCRADIO_FOLDER_TYPE = "android.media.extra.EXTRA_BCRADIO_FOLDER_TYPE". One of the following values:
    • BCRADIO_FOLDER_TYPE_PROGRAMS = 1. Currently available programs.
    • BCRADIO_FOLDER_TYPE_FAVORITES = 2. Favorites.
    • BCRADIO_FOLDER_TYPE_BAND = 3. All physical channels for a given band.

    There is no need to define any radio-specific custom metadata fields, as all the relevant data fits into the existing MediaBrowser.MediaItem scheme:

    • Program name (RDS PS, DAB service name). MediaDescription.getTitle.
    • FM frequency. URI (see ProgramSelector) or MediaDescription.getTitle (if an entry is in the BROADCASTRADIO_FOLDER_TYPE_BAND folder).
    • Radio-specific identifiers (RDS PI, DAB SId). MediaDescription.getMediaUri parsed to ProgramSelector.

    Typically, there is no need to fetch FM frequency for an entry on the current program or Favorites list (as the client should operate on media IDs). However, if such a need were to arise (for example, for display purposes), it's present in the URI and can be parsed to ProgramSelector. That said, it's not recommended the URI be used to select items within the current session. For details, see ProgramSelector.

    To avoid performance or binder-related issues, the MediaBrowser service must support pagination:

    Note: By default, pagination is implemented by default in the onLoadChildren() variant without options handling.

    Related entries from all types of lists (raw channels, programs found and favorites) may have different mediaIds (it's up to the radio app; support library will have them different). The URIs (in ProgramSelector form) differ between raw channels and programs found in most cases (except for FM without RDS), but are mostly the same between programs found and favorites (except, for example, when AF was updated).

    Having different mediaIds for entries from different types of lists makes it possible to take different actions on them. You can traverse either the Favorites list or the All Programs list on onSkipToNext, depending on the folder of recently selected MediaItem (see MediaSession).

    Special tune actions

    Program list enables users to tune to a specific station, but does not allow users to make general requests such as "Tune to FM", which could result in tuning to a recently listened to station on the FM band.

    To support such actions, some top-level directories have the FLAG_PLAYABLE flag set (along with FLAG_BROWSABLE for folders).

    Action Tunes to How to issue
    Play radio Any radio channel startService(ACTION_PLAY_BROADCASTRADIO)

    OR,

    playFromMediaId(MediaBrowser.getRoot())
    Play FM Any FM channel Play from the mediaId of the FM band.

    The determination of which program to tune to is up to the app. This is typically the most recently tuned to channel from the given list. For details on ACTION_PLAY_BROADCASTRADIO, see General play intents.

    Discovery and service connection

    PackageManager can directly find the MediaBrowserService serving broadcast radio tree. To do so, call resolveService with the ACTION_PLAY_BROADCASTRADIO intent (see General play intents) and MATCH_SYSTEM_ONLY flag. To find all services that serve radio (there may be more than one; for example, separate AM/FM and satellite), use queryIntentServices.

    The resolved service handles the android.media.browse.MediaBrowserService bind intent, too. This is verified with GTS.

    To connect to the selected MediaBrowserService, create MediaBrowser instance for a given service component and connect. After establishing the connection, a handle to MediaSession can be obtained via getSessionToken.

    The Radio app can restrict client packages allowed to connect in an onGetRoot implementation of their service. The app should allow system apps to connect without whitelisting. For details about whitelisting, see Accept the Assistant app package and signature.

    If the source-specific app (for example, a radio app) is installed on a device without such source support, it would still advertise itself as handling the ACTION_PLAY_BROADCASTRADIO intent, but its MediaBrowser tree would not contain radio-specific tags. Thus, a client willing to check if a given source is available on a device, must:

    1. Discover the radio service (call resolveService for ACTION_PLAY_BROADCASTRADIO).
    2. Create MediaBrowser and then connect to it.
    3. Determine the presence of MediaItem with EXTRA_BCRADIO_FOLDER_TYPE extra.

    Note: In most cases, the client must scan all available MediaBrowser trees to detect all available sources for a given device.

    Band names

    Band list is represented by a set of top-level directories with a folder type tag set to BCRADIO_FOLDER_TYPE_BAND. Their MediaItem's titles are localized strings representing band names. In most cases it will be the same as English translation, but the client can't depend on that assumption.

    To provide a stable mechanism for looking up certain bands, an extra tag is added for band folders, EXTRA_BCRADIO_BAND_NAME_EN. This is a non-localized name of the band and can only take one of these predefined values:

    • AM
    • FM
    • DAB

    If the band is not on this list, the band name tag should not be set. However, if the band is on the list, it must have a tag set. HD radio doesn't enumerate separate bands as it uses the same underlying medium as AM/FM.

    General play intents

    Each app dedicated for playing given source (like radio or CD) must handle a general play intent to start playing some content possibly from inactive state (for example, after boot). It's up to the app how to select content to play, but it's usually the recently played radio program or CD track.There is a separate intent defined for each audio source:

    • android.car.intent.action.PLAY_BROADCASTRADIO
    • android.car.intent.action.PLAY_AUDIOCD: CD-DA or CD-Text
    • android.car.intent.action.PLAY_DATADISC: Optical data disc like CD/DVD, but not CD-DA (may be Mixed Mode CD)
    • android.car.intent.action.PLAY_AUX: Without specifying which AUX port
    • android.car.intent.action.PLAY_BLUETOOTH
    • android.car.intent.action.PLAY_USB: Without specifying which USB device
    • android.car.intent.action.PLAY_LOCAL: Local media storage (built-in flash)

    Intents were chosen to be used for general play command, because they solve two problems at once: the general play command itself and service discovery. Additional benefit of having such intent would be a possibility to execute such simple action without opening MediaBrowser session.

    Service discovery is actually the more important problem solved with these intents. Procedure for service discovery is easy and unequivocal this way (see Discovery and service connection).

    To make some client implementations easier, there is an alternative way of issuing such Play command (that also has to be implemented by the radio app): issuing playFromMediaId with the rootId of the root node (used as mediaId). While the root node is not meant to be playable, its rootId is an arbitrary string which can be made to be consumable as mediaId. However, clients are not required to understand this nuance.

    ProgramSelector

    While mediaId is enough to select a channel from the MediaBrowserService, it becomes bound to a session and not consistent between providers. In some cases the client may need an absolute pointer (such as an absolute frequency) to maintain it between sessions and devices.

    In the era of digital radio broadcasts, a bare frequency is not sufficient to tune to a specific station. Therefore, use ProgramSelector to tune to an analog or digital channel. ProgramSelector consists of two parts:

    • Primary identifier. A unique and stable identifier for a given radio station that doesn't change but may not be enough to tune to that station. For example, RDS PI code, which may be translated to the call sign in the US.
    • Secondary identifiers. Additional identifiers useful for tuning to that station (for example, frequency), possibly including identifiers from other radio technologies. For example, a DAB station may have an analog broadcasting fallback.

    To enable ProgramSelector to fit into the MediaBrowser- or MediaSession-based solution, define a URI schema to serialize it. The schema is defined as follows:

    broadcastradio://program/<primary ID type>/<primary ID>?
    <secondary ID type>=<secondary ID>&<secondary ID type>=<secondary ID>
    

    In this example, the secondary Identifiers portion (after the question mark (?)) is optional and can be removed to provide a stable identifier for use as mediaId. For example:

    • broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=88500&AMFM_FREQUENCY=103300
    • broadcastradio://program/AMFM_FREQUENCY/102100
    • broadcastradio://program/DAB_SID_EXT/14895264?RDS_PI=1234

    The authority part (AKA host) of program provides some room for scheme extension in the future. The identifier type strings are precisely specified as their names in the HAL 2.x definition of IdentifierType and the value format is a decimal or hexadecimal (with 0x prefix) number.

    All vendor-specific identifiers are represented by the VENDOR_ prefix. For example, VENDOR_0 for VENDOR_START and VENDOR_1 for VENDOR_START plus 1. Such URIs are specific to the radio hardware on which they were generated and cannot be transferred between devices made by different OEMs.

    These URIs must be assigned to each MediaItem under the top-level radio folders. In addition, the MediaSession must support both playFromMediaId and playFromUri. However, the URI is primarily intended for radio metadata extraction (such as FM frequency) and persistent storage. There is no guarantee the URI will be available for all media items (for example, when the primary ID type is not yet supported by the framework). On the other hand, Media ID always works. It is not recommended that clients use URI to select items from the current MediaBrowser session. Instead, use playFromMediaId. That said, it is not optional for the serving app and missing URIs are reserved for well-justified cases.

    The initial design used a single colon instead of the :// sequence after the scheme part. However, the former is not supported by android.net.Uri for absolute hierarchical URI references.

    Other source types

    Other audio sources can be handled similarly. For example, auxiliary input and the Audio CD player.

    A single app may serve multiple types of sources. In such cases, it's recommended you create a separate MediaBrowserService for each type of source. Even in a set-up with multiple served sources/MediaBrowserServices, it's strongly recommended to have a single MediaSession within a single app.

    Audio CD

    Similar to Audio CD in that the app that serves such disks would expose MediaBrowser with a single browsable entry (or more, if the system has a CD changer), which in turn would contain all tracks of a given CD. If the system does not have the knowledge about the tracks on every CD (for example, when all disks are inserted in a cartridge at once and it doesn't read them all), then MediaItem for the entire disk would be just PLAYABLE, not BROWSABLE plus PLAYABLE. If there is no disk in a given slot, the item would be neither PLAYABLE nor BROWSABLE (but each slot must always be present in the tree).

     Audio CD tree structure
    Figure 3. Audio CD tree structure.

    These entries would be marked in a similar way that broadcast radio folders are; they would contain additional extra fields defined in the MediaDescription API:

    • EXTRA_CD_TRACK: For every MediaItem on Audio CD, 1-based track number.
    • EXTRA_CD_DISK: 1-based disk number.

    For CD-Text enabled system and compatible disk, the top-level MediaItem would have a title of the disk. Similarly, the MediaItems for tracks, would have a title of the track.

    Auxiliary input

    The app that serves auxiliary input exposes a MediaBrowser tree with a single entry (or more, when multiple ports exist) representing the AUX in port. The respective MediaSession takes its mediaId and switches to that source after getting the playFromMediaId request.

    AUX tree structure
    Figure 4. AUX tree structure.

    Each AUX MediaItem entry would have an extra field EXTRA_AUX_PORT_NAME set to the non-localized name of the port without the "AUX" phrase. For example, "AUX 1" would have be set to "1", "AUX front" to "front" and "AUX" to an empty string. In non-English locales, the name tag would remain the same English string. Unlikely as for EXTRA_BCRADIO_BAND_NAME_EN, the values are OEM-defined and not constrained to a predefined list.

    If the hardware can detect devices connected to the AUX port, the hardware should mark the MediaItem as PLAYABLE, only if input is connected. The hardware should still be enumerated (but not PLAYABLE) if nothing was connected to this port. If the hardware has no such capability, the MediaItem must always be set to PLAYABLE.

    Extra fields

    Define the following fields:

    • EXTRA_CD_TRACK = "android.media.extra.CD_TRACK"
    • EXTRA_CD_DISK = "android.media.extra.CD_DISK"
    • EXTRA_AUX_PORT_NAME = "android.media.extra.AUX_PORT_NAME"

    Client needs to review the top-level MediaItems for elements having the EXTRA_CD_DISK or EXTRA_AUX_PORT_NAME extra field set.

    Detailed examples

    The following examples address the MediaBrowser tree structure for source types that are part of this design.

    Broadcast radio MediaBrowserService (handles ACTION_PLAY_BROADCASTRADIO):

    • Stations (browsable)EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_PROGRAMS
      • BBC One (playable) URI: broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=90500
      • ABC 88.1 (playable) URI: broadcastradio://program/RDS_PI/5678?AMFM_FREQUENCY=88100
      • ABC 88.1 HD1 (playable) URI: broadcastradio://program/HD_STATION_ID_EXT/158241DEADBEEF?AMFM_FREQUENCY=88100&RDS_PI=5678
      • ABC 88.1 HD2 (playable) URI: broadcastradio://program/HD_STATION_ID_EXT/158242DEADBEFE
      • 90.5 FM (playable) - FM without RDSURI: broadcastradio://program/AMFM_FREQUENCY/90500
      • 620 AM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/620
      • BBC One (playable) URI: broadcastradio://program/DAB_SID_EXT/1E24102?RDS_PI=1234
    • Favorites (browsable, playable)EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_FAVORITES
      • BBC One (playable) URI: broadcastradio://program/RDS_PI/1234?AMFM_FREQUENCY=101300
      • BBC Two (not playable)URI: broadcastradio://program/RDS_PI/1300?AMFM_FREQUENCY=102100
    • AM (browsable, playable): EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="AM"
      • 530 AM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/530
      • 540 AM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/540
      • 550 AM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/550
    • FM (browsable, playable): EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="FM"
      • 87.7 FM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/87700
      • 87.9 FM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/87900
      • 88.1 FM (playable) URI: broadcastradio://program/AMFM_FREQUENCY/88100
    • DAB (playable): EXTRA_BCRADIO_FOLDER_TYPE=BCRADIO_FOLDER_TYPE_BANDEXTRA_BCRADIO_BAND_NAME_EN="DAB"

    Audio CD MediaBrowserService (handles ACTION_PLAY_AUDIOCD):

    • Disc 1 (playable) EXTRA_CD_DISK=1
    • Disc 2 (browsable, playable) EXTRA_CD_DISK=2
      • Track 1 (playable) EXTRA_CD_TRACK=1
      • Track 2 (playable) EXTRA_CD_TRACK=2
    • My music CD (browsable, playable) EXTRA_CD_DISK=3
      • All By Myself (playable) EXTRA_CD_TRACK=1
      • Reise, Reise (playable) EXTRA_CD_TRACK=2
    • Empty slot 4 (not playable) EXTRA_CD_DISK=4

    AUX MediaBrowserService (handles ACTION_PLAY_AUX):

    • AUX front (playable) EXTRA_AUX_PORT_NAME="front"
    • AUX rear (playable) EXTRA_AUX_PORT_NAME="rear"