By implementing Android.Service.Media.MediaBrowserService, we are providing Android Auto the ability to browse through your media playlists. From the car dashboard, the user will be able to click through the playlists and select any podcast he or she wants to listen to:
- Open Visual Studio 2017.
- Go to File | Open | Project/Solution, go to your MyPodCast directory, and choose the MyPodCast.sln file.
- Right-click on MediaService folder | Add | New Item | and select Visual C# Class and name the file MusicService.cs
- Click Add
- Open MusicService.cs by double-clicking the file.
- Let the MusicService class implement Android.Service.Media.MediaBrowserService
- Add the MusicService class's attributes:
- Add the Service class attribute with Export set to true, Label set to "Henry Podcast Service", and Name set to "com.henry.mypodcast.service". The label will be displayed along with other media players such as Spotify or Google Play Music on your car dashboard .
- Add IntentFilter, which contains new[] { android.media.browse.MediaBrowserService}. IntentFilter allows MusicService to provide service only to the application that is requesting the media browsing service.
The following is the completed code after steps 7 and 8:
[Service(Exported = true, Label = "Henry Podcast Service", Name = "com.henry.mypodcast.service")]
[IntentFilter(new[] { "android.media.browse.MediaBrowserService" })]
public class MusicService : Android.Service.Media.MediaBrowserService
The following screenshot shows the Henry Podcast Service label in Android Auto:
- Add the global public and private variables that you will be using throughout the MusicService.
- The following describes the variables declared in MusicService:
- CustomActionFavorite set to "com.henry.mypodcast.FAVORITE" will be used to reserve space in the Android Auto media player to handle custom actions.
- _playingQueue will contain podcasts that will be played one after another.
- _isStarted tracks whether or not MusicService has started.
- _session contains MediaSession, which will interact with MediaController, which in turn controls media player functions such as play, pause, volume, next, and previous.
- _musicPlayer contains the MusicPlayer class, which will be primarily used for creating an Android phone media player UI. MusicPlayer controls Android media player's play, pause, next, previous, and media browsing functions.
- _musicProvider is an abstraction layer that provides streaming of media contents, cover images, and media information.
- _packageFinder is a security layer that ensures that only the selected external application accesses your MusicService.
The following code shows the declared variables for the MusicService class:
private const string CustomActionFavorite = "com.henry.mypodcast.FAVORITE";
private List<MediaSession.QueueItem> _playingQueue;
private int _currentIndexQueue;
private bool _isStarted;
private MediaSession _session;
private MusicPlayer _musicPlayer;
private MusicProvider _musicProvider;
private PackageFinder _packageFinder;
- Create a public override void OnCreate() method that will get invoked whenever an external application such as Android Auto makes a request to use your MusicService.
- The OnCreate() method instantiates the _playingQueue, _musicProvider, _packageFinder, _musicPlayer, and _session variables. To create _musicPlayer, the constructor takes MusicService and _musicProvider because _musicPlayer uses MusicService to browse and play media content, and _musicProvider provides media content sources.
The following code shows the variables being instantiated mentioned in step 11:
base.OnCreate();
_playingQueue = new List<MediaSession.QueueItem>();
_musicProvider = new MusicProvider();
_packageFinder = new PackageFinder();
_musicPlayer = new MusicPlayer(this, _musicProvider);
- OnCreate() instantiates object mediaCallback: var mediaCallback = this.CreateMediaSessionCallback()
- Next, create a private MediaSessionCallback CreateMediaSessionCallback() method that returns MediaSessionCallback. CreateMediaSessionCallback handles all media interactions, such as browse, play, forward, previous, pause, search, add, and favorite.
- The following media callbacks are implemented in the private MediaSessionCallback CreateMediaSessionCallback() method:
- OnPlayImpl: On receiving a request to play media, it checks to see whether any podcasts are queued in _playingQueue. If no podcast is queued, then it will get a random podcast from _musicProvider. Otherwise, it will play the first podcast in the queue.
The following code shows OnPlayImpl:
mediaCallback.OnPlayImpl = () => {
if (_playingQueue == null || _playingQueue.Count != 0){
_playingQueue = new List<MediaSession.QueueItem>(_musicProvider.GetRandomQueue());
_session.SetQueue(_playingQueue);
_session.SetQueueTitle("Random music");
_currentIndexQueue = 0;
}
if (_playingQueue != null && _playingQueue.Count != 0)
HandlePlayRequest();
};
-
- OnSkipToQueueItemImpl: It receives QueueId_playingQueue; when media with that specific QueueId is found, it plays that podcast.
The following code shows OnSkipToQueueItemImpl:
mediaCallback.OnSkipToQueueItemImpl = (id) => {
if (_playingQueue != null && _playingQueue.Count != 0)
{
_currentIndexQueue = -1;
int index = 0;
foreach (var item in _playingQueue)
{
if (id == item.QueueId)
_currentIndexQueue = index;
index++;
}
HandlePlayRequest();
}
};
-
- OnSeekToImpl: This is called when resume is clicked. The podcast will resume from this position.
The following code shows OnSeekToImpl:
mediaCallback.OnSeekToImpl = (pos) => {
_musicPlayer.SeekTo((int)pos);
};
-
- OnPlayFromMediaIdImpl: MusicService has two categories: All Podcasts and Month. All Podcasts is the top-level category and Month is the subcategory. Under Month, there will be a list of podcasts. Every podcast's media has a mediaId and this is encoded with category information and mediaId. For example, mediaId can contain January/1, which is decoded as podcastId = 1 in the January subcategory. Using the decoded information, you can quickly search _musicProvider to extract all podcasts that belong to January and add them to _playingQueue.
The following screenshot shows the list of podcasts that belong to January; clicking any of the podcasts displayed will invoke OnPlayFromMediaIdImpl:
The following code shows OnPlayFromMediaIdImpl:
mediaCallback.OnPlayFromMediaIdImpl = (mediaId, extras) => {
_playingQueue = _musicProvider.GetPlayingQueue(mediaId);
_session.SetQueue(_playingQueue);
string[] hierarchies = HierarchyHelper.GetHierarchy(mediaId);
string month = hierarchies != null && hierarchies.Length == 2 ? hierarchies[1] : string.Empty;
var queueTitle = $"{month} Podcasts";
_session.SetQueueTitle(queueTitle);
if (_playingQueue != null && _playingQueue.Count != 0) {
_currentIndexQueue = -1;
int index = 0;
foreach (var item in _playingQueue){
if (mediaId == item.Description.MediaId)
_currentIndexQueue = index;
index++;
}
if (_currentIndexQueue< 0)
Logger.Error($"OnPlayFromMediaIdImpl: media ID {mediaId} not be found.");
else
HandlePlayRequest();
}
};
-
- OnPauseImpl: Pauses the currently playing media.
The following code shows OnPauseImpl:
mediaCallback.OnPauseImpl = () => {
OnPause();
};
-
- OnStopImpl: Stops the currently playing media.
The following code shows OnStopImpl:
mediaCallback.OnStopImpl = () => {
OnStop(null);
};
-
- OnSkipToNextImpl: Plays the next piece of media in the play list.
The following code shows OnSkipToNextImpl:
mediaCallback.OnSkipToNextImpl = () => {
_currentIndexQueue++;
if (_playingQueue != null && _currentIndexQueue >= _playingQueue.Count)
_currentIndexQueue = 0;
if (this.isIndexPlayable(_currentIndexQueue, _playingQueue))
HandlePlayRequest();
else
OnStop("Cannot skip");
};
-
- OnSkipToPreviousImpl: Plays the previous media in the play list.
The following code shows OnSkipToPreviousImpl:
mediaCallback.OnSkipToPreviousImpl = () => {
_currentIndexQueue--;
if (_playingQueue != null && _currentIndexQueue < 0)
_currentIndexQueue = 0;
if (this.isIndexPlayable(_currentIndexQueue, _playingQueue))
HandlePlayRequest();
else
OnStop("Cannot skip");
};
-
- OnCustomActionImpl: Handles the favorite custom action by adding that specific podcast to the favorite list. This is to demonstrate how to create a custom action in Android Auto.
The following code shows OnCustomActionImpl:
mediaCallback.OnCustomActionImpl = (action, extras) => {
if (CustomActionFavorite == action)
{
var track = GetCurrentPlayingMusic();
if (track != null){
var musicId = track.GetString(MediaMetadata.MetadataKeyMediaId);
_musicProvider.SetFavorite(musicId, !_musicProvider.IsFavorite(musicId));
}
UpdatePlaybackState(null);
}
};
-
- OnPlayFromSearchImpl: In Android Auto, when the user says "OK Google, play Henry test on Henry Podcast Service," this call back will be invoked. _musicProvider.GetPlayingQueueFromSearch will search through the list of podcast titles and return a matching search query.
The following code shows OnPlayFromSearchImpl:
mediaCallback.OnPlayFromSearchImpl = (query, extras) => {
if (string.IsNullOrEmpty(query))
_playingQueue = new List<MediaSession.QueueItem>(_musicProvider.GetRandomQueue());
else
_playingQueue = new List<MediaSession.QueueItem>(_musicProvider.GetPlayingQueueFromSearch(query));
_session.SetQueue(_playingQueue);
if (_playingQueue != null && _playingQueue.Count != 0)
{
_currentIndexQueue = 0;
HandlePlayRequest();
}
else
OnStop("0 Found.");
};
- Back in OnCreate(), instantiate _session = new MediaSession(this, "HenryPodcast"), where this is MusicService itself and "HenryPodcast" is the session name.
- Next, in OnCreate(), set MusicService SessionToken = _session.SessionToken. Any time an external application requests MusicService, a session will be created with a unique session token.
- Next, in OnCreate(), add the mediaCallback that you created in step 12 to the session: _session.SetCallback(mediaCallback).
- Next, in OnCreate(), add pendingIntent to _session.SetSessionActivity. pendingIntent allows the _session that you created in step 13 to control the media operation in the Android mobile media player. When Android Auto takes control, your Android phone will be locked and the Android media player that you will be creating later will be in the back ground. You want the MusicService session serving the request to Android Auto to also control the Android media player. For example, if Android Auto is playing the podcast, you want the Android media player to be in sync and doing the same thing, because if the user disengages from Android Auto and unlocks the Android phone, you want your Android media player to continue playing the same podcast.
The following code shows creating and setting pendingIntent to the session in OnCreate:
Context context = ApplicationContext;
var intent = new Intent(context, typeof(MainActivity));
var pendingIntent = PendingIntent.GetActivity(context, 99, intent, PendingIntentFlags.UpdateCurrent);
_session.SetSessionActivity(pendingIntent);
- Finally, OnCreate() creates extraBundle, which will reserve the actions in Android Auto that you can control by receiving a callback notification when those reserved actions are performed. For example, you may want to be notified when the play, previous, next, pause, and queue actions are performed. Then, add extraBundle to the _session.SetExtras(extraBundle) session.
The following reserved key words are set in extraBundle:
-
- com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE
- com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS
- com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT
- com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_PLAY_PAUSE
The following code shows the code reserving the play, previous, next, pause, and queue actions:
var extraBundle = new Bundle();
extraBundle.PutBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_QUEUE", true);
extraBundle.PutBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_PREVIOUS", true);
extraBundle.PutBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_SKIP_TO_NEXT", true);
extraBundle.PutBoolean(
"com.google.android.gms.car.media.ALWAYS_RESERVE_SPACE_FOR.ACTION_PLAY_PAUSE", true);
_session.SetExtras(extraBundle);
- Create public override void OnDestroy(), which will get called when MusicService is no longer needed. Here, you need to release the resources and stop MusicService:
The following code shows the OnDestroy() method:
public override void OnDestroy()
{
Logger.Debug("OnDestroy");
OnStop(null);
_session.Release();
}
- Create public override BrowserRoot OnGetRoot(string clientPackageName, int clientUid, Bundle rootHints), which gets called the very first time when the user tries to browse through your podcast media. Here, you will be performing a security check to make sure the client requesting MusicsService has access by invoking _packageFinder.Find(clientPackageName). If you verify that the client has access, you return new BrowserRoot(HierarchyHelper.PodcastRoot, null), where you can start to browse your media list. _packageFinder.Find allows only two external clients to access Android Auto, "com.google.android.projection.gearhead" and your MyPodCast, "com.henry.mypodcast".
The following code shows the OnGetRoot implementation:
public override BrowserRoot OnGetRoot(string clientPackageName, int clientUid, Bundle rootHints)
{
Logger.Debug($"OnGetRoot: clientPackageName={clientPackageName}");
if (_packageFinder.Find(clientPackageName))
return new BrowserRoot(HierarchyHelper.PodcastRoot, null);
else {
Logger.Warn($"OnGetRoot: clientPackageName={clientPackageName} ignored");
return null;
}
}
The following code shows the PackageFinder class implementation:
public class PackageFinder
{
private List<string> _allowedAppNames;
public PackageFinder()
{
_allowedAppNames = new List<string>() {
"com.google.android.projection.gearhead",
"com.henry.mypodcast"
};
}
public bool Find(string clientPackageName)
{
return _allowedAppNames.Contains(clientPackageName);
}
}
- Create public override void OnLoadChildren(string parentId, Result result), which gets called when the user is clicking through the media categories. The categories start with All Podcasts, and All Podcasts has two child categories, January and March. When either January or March is clicked on, it will show the podcasts that the user can click to play. The very first time OnLoadChildren is called, _musicProvider.RetrieveMedia will be invoked to retrieve the play list and LoadChildrenImpl will handle all the browsing requests.
The following code shows the OnLoadChildren implementation:
public override void OnLoadChildren(string parentId, Result result)
{
if (!_musicProvider.IsInitialized) {
result.Detach();
_musicProvider.RetrieveMedia(success => {
if (success)
LoadChildrenImpl(parentId, result);
else {
UpdatePlaybackState("Unable to get the data.");
result.SendResult(new JavaList<MediaBrowser.MediaItem>());
}
});
}
else
LoadChildrenImpl(parentId, result);
}
- Create private void LoadChildrenImpl(string parentId, Result result), which will load categories while the user is browsing through the media categories. LoadChildrenImpl is composed of three sections: loading the root of the browsing category, loading All Podcasts, and loading monthly podcasts for January and March. On All Podcasts, January and March categories MediaBrowser.MediaItems are created with the title, the subtitle, and the mediaId set to the next category to load on click. Finally, the created MediaItems are sent using result.SendResult(mediaItems).
The following table shows the category system of MusicService and which LoadChildrenImpl will load for each of the categories:
Root category |
When application is initialized |
When All Podcasts is clicked |
When Month (January or March) is clicked |
ROOT -> (Can be browsed) |
All Podcasts -> (Can be browsed) |
January -> (Can be browsed) |
Henry1 Test (Playable music) Henry2 Test (Playable music) Henry3 Test (Playable music) |
March -> (Can be browsed) |
Henry4 Test (Playable music) |
-
- In ROOT, MediaId is set to BY_MONTH and title is set to All Podcasts. When the user clicks on All Podcasts, LoadChildrenImpl will send a parantId that contains BY_MONTH, which will instruct the method to build the correct month's list, January or March.
The following code shows the ROOT category loading All Podcasts:
if (HierarchyHelper.PodcastRoot == parentId) {
mediaItems.Add(new MediaBrowser.MediaItem(
new MediaDescription.Builder()
.SetMediaId(HierarchyHelper.PodcastsByMonth)
.SetTitle("All Podcasts")
.SetIconUri(Android.NET.Uri.Parse(
"android.resource://com.henry.mypodcast/drawable/ic_by_genre"))
.SetSubtitle("Podcasts By Month")
.Build(), MediaItemFlags.Browsable));
}
The following screenshot shows the loaded ROOT category:
-
- In BY_MONTH, MediaId is dynamically created using BY_MONTH/[month name] (for example, BY_MONTH/January). The title is set to the name of the month, and the subtitle is set to January Podcasts and March Podcasts.
The following code shows the section loading the BY_MONTH category:
else if (HierarchyHelper.PodcastsByMonth == parentId)
{
foreach (var month in _musicProvider.Months) {
var item = new MediaBrowser.MediaItem(
new MediaDescription.Builder()
.SetMediaId(HierarchyHelper.PodcastsByMonth + HierarchyHelper.CategorySeparator + month)
.SetTitle(month)
.SetSubtitle($"{month} Podcasts")
.Build(), MediaItemFlags.Browsable);
mediaItems.Add(item);
}
}
The following screenshot shows the loaded list of months:
-
- In BY_MONTH/January and BY_MONTH/March, the MediaId contains a specially encoded value using By_MONTH/[month name]|[podcast trackId] (for example, BY_MONTH/January/1). MediaBrowser.MediaItem contains the playable podcasts' media content and it will be set to MediaItemFlags.Playable so that when the user clicks on the media, it will play the content.
The following code shows loading playable media content for the selected month:
else if (parentId.StartsWith(HierarchyHelper.PodcastsByMonth))
{
var month = HierarchyHelper.GetHierarchy(parentId)[1];
foreach (var track in _musicProvider.GetMusicsByMonth(month))
{
var hierarchyAwareMediaID = HierarchyHelper.EncodeMediaID(
track.Description.MediaId, HierarchyHelper.PodcastsByMonth, month);
var trackCopy = new MediaMetadata.Builder(track)
.PutString(MediaMetadata.MetadataKeyMediaId, hierarchyAwareMediaID)
.Build();
var bItem = new MediaBrowser.MediaItem(
trackCopy.Description, MediaItemFlags.Playable);
mediaItems.Add(bItem);
}
}
The following screenshot shows loading the BY_MONTH/January playlist: