Implementing MusicService (Android MediaBrowserService)

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:

  1. Open Visual Studio 2017.
  2. Go to File | Open | Project/Solution, go to your MyPodCast directory, and choose the MyPodCast.sln file.
  3. Right-click on MediaService folder | Add | New Item | and select Visual C# Class and name the file MusicService.cs
  1. Click Add
  2. Open MusicService.cs by double-clicking the file.
  3. Let the MusicService class implement Android.Service.Media.MediaBrowserService
  4. 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:

Label: Henry Podcast Service
  1. Add the global public and private variables that you will be using throughout the MusicService.
  2. 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;
  1. 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.
  1. 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);
  1. OnCreate() instantiates object mediaCallback: var mediaCallback = this.CreateMediaSessionCallback()
  2. 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.
  3. 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();
};

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();
}
};

The following code shows OnSeekToImpl:

mediaCallback.OnSeekToImpl = (pos) => {
_musicPlayer.SeekTo((int)pos);
};

The following screenshot shows the list of podcasts that belong to January; clicking any of the podcasts displayed will invoke OnPlayFromMediaIdImpl:

List of podcasts in January

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();
}
};

The following code shows OnPauseImpl:

mediaCallback.OnPauseImpl = () => {
OnPause();
};

The following code shows OnStopImpl:

 mediaCallback.OnStopImpl = () => {
OnStop(null);
};

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");
};

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");
};

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);
}
};

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.");
};
  1. Back in OnCreate(), instantiate _session = new MediaSession(this, "HenryPodcast"), where this is MusicService itself and "HenryPodcast" is the session name.
  2. 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.
  3. Next, in OnCreate(), add the mediaCallback that you created in step 12 to the session: _session.SetCallback(mediaCallback).
  4. 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);
  1. 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:

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);
  1. 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();
}
  1. 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);
}
}
  1. 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);
}
  1. 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)

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:

Loading ROOT category

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:

Loaded list of months

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:

BY_MONTH/January|1 and BY_MONTH/January|2