As was noted earlier in the book, you can use MediaRouter
to identify media routes, such as those published by devices like Google’s Chromecast.
Specifically, remote playback routes let you write apps that tell other devices,
like the Chromecast, to play back media on your behalf.
However, not only can you write clients for remote playback routes, you can write providers of those routes. Perhaps you are working with a hardware manufacturer that is creating a Chromecast-like device. Perhaps you want to allow your app, running on a Fire TV or an Android HDMI stick, to be controlled by a user’s phone or tablet. Or perhaps you are trying to tie Android into specialized media hardware that does not communicate by conventional means (e.g., wireless speakers that do not use normal Bluetooth profiles).
This chapter will outline how you can create code that will publish media routes
to users of MediaRouter
, so that you can then take those requests and forward them
to a remote device.
This chapter assumes that you have read the chapter on MediaRouter
.
For the purposes of this chapter:
Google’s Chromecast is a nice little device. However, it has issues:
Some of these issues can be mitigated by the use of MediaRouter
and RemotePlaybackClient
instead of the proprietary Cast SDK. You are not bound by any particular license
terms (beyond the norm for Android development) and the implementation of the media framework
is open.
However, to make this work, the client device needs to know how to talk to the player device.
The good news is that the media routing framework in Android supports plug-in media
route providers for just this purpose. The OS ships with such a provider for the
Chromecast, and you can create your own providers to talk to whatever else you would
like to talk to. The user can then install a small app on their client device that
implements this media route provider, and any apps already on their client device that
use classes like RemotePlaybackClient
will automatically be able to cast their
desired content to the player device.
The guts of this come in the form of a MediaRouteProvider
. Your custom subclass
of MediaRouteProvider
will:
http
and rtsp
)MediaRouter
, RemotePlaybackClient
, and the
like, for you to forward along asynchronously to the player deviceDepending upon your use case, you could elect to keep the MediaRouteProvider
private to your application. That way, your app can cast to the player device, but
no other apps can. Or, you can make your MediaRouteProvider
available to all apps
on the device, with the media routing framework taking care of the IPC details to
have those apps tell your MediaRouteProvider
what the player device should do.
Of course, this assumes the existence of some player device that is not supported by Android out of the box. Since Android only really supports Chromecast, external displays (e.g., HDMI, MHL, Miracast), and some Bluetooth options (e.g., external speakers) for media routes, there are countless player devices that need additional help. These will run the gamut from devices from major players (e.g., Amazon’s Fire TV) to no-name devices (e.g., Android HDMI “sticks”).
Some player devices will run Android. In that case, you would be writing a player app
that would run on the player device that would be the recipient of commands sent to
it from your MediaRouteProvider
on the client device. For example, if you write a
video player app, you could augment it with remote control capability driven by a
MediaRouteProvider
on a client device, turning your player app and anything it can
run on (e.g., Fire TV, OUYA game console) into a Chromecast-like environment.
Some player devices will not run Android. If they offer some existing remote control
over-the-air protocol, you could create a MediaRouteProvider
that speaks that protocol.
Or, perhaps the player devices are programmable, just not via Android (e.g., a Linux
program for XMBC), in which case you might be able to write both ends of the communications
channel.
Somehow, the data from the MediaRouteProvider
needs to get to the player device
(and, where relevant, the player app). Likely candidates include Bluetooth, regular
WiFi (if both devices are on the same network), and WiFi Direct.
However, in principle, anything is possible. For example, there is nothing stopping you
from sending MediaRouteProvider
commands to some Web server out on the Internet,
which forwards them to some distant location for use. That would be a bit unusual –
normally, the user of the client device is controlling something she can see — but it
certainly could be done.
The biggest thing to watch out for is the addressability of the media to be played
back. There is little point in connecting a MediaRouteProvider
to some player device,
then not have the ability for the player device to access the media that the
client device is requesting. The expected pattern is that the media is hosted in some
(relatively) central location, like a Web server. However, once again, anything is
possible. If you want to have some sort of server on the client device, to allow the player
device to play back media from it, and you believe that you can adequate secure this,
you are welcome to do so.
As noted earlier, the core of all of this is a custom MediaRouteProvider
. Google
supplies a sample application
for creating such a MediaRouteProvider
. However, it is overly complex, and it is undocumented.
This chapter will focus instead on the
MediaRouter/RouteProvider
sample project. This is a clone of the MediaRouter/RemotePlayback
sample project
covered earlier in this book, with the addition of a custom
MediaRouteProvider
.
A MediaRouteProvider
advertises — whether to its own app’s MediaRouter
or to the
entire device — what sorts of actions it can perform. For example, a remote playback
route provider needs to support actions like play, pause, resume, and stop of some piece
of media.
The way this is handled in the media routing framework is via a series of IntentFilter
objects.
Since IntentFilter
objects do not need a Context
to be created, it is safe to define
them statically, if desired. That’s what we do in DemoRouteProvider
, a custom subclass
of MediaRouteProvider
. It declares a pair of static final IntentFilter
objects,
ifPlay
and ifControl
, which are then configured in a static
initialization block:
private static final IntentFilter ifPlay=new IntentFilter();
private static final IntentFilter ifControl=new IntentFilter();
static {
ifPlay.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
ifPlay.addAction(MediaControlIntent.ACTION_PLAY);
ifPlay.addDataScheme("http");
ifPlay.addDataScheme("https");
ifPlay.addDataScheme("rtsp");
try {
ifPlay.addDataType("video/*");
}
catch (MalformedMimeTypeException e) {
throw new RuntimeException("Exception setting MIME type", e);
}
ifControl.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
ifControl.addAction(MediaControlIntent.ACTION_PAUSE);
ifControl.addAction(MediaControlIntent.ACTION_RESUME);
ifControl.addAction(MediaControlIntent.ACTION_STOP);
ifControl.addAction(MediaControlIntent.ACTION_GET_STATUS);
ifControl.addAction(MediaControlIntent.ACTION_SEEK);
}
Both stipulate that they are looking for Intent
objects in the
MediaControlIntent.CATEGORY_REMOTE_PLAYBACK
category. This category is used for all
media routing Intent
s that form the foundation of the routing framework.
ifPlay
is defined as supporting MediaControlIntent.ACTION_PLAY
, stating that we know
how to play back some content. The qualifications for “some content” are handled via
scheme and type constraints placed on the IntentFilter
. Here, we limit the content
to be URLs that might be reachable by a playback device (http
, https
, rtsp
) and
have a MIME type matching video/*
. Hence, we are stating that we can play back streaming
video.
ifControl
sets up the remaining actions that we support:
MediaControlIntent.ACTION_PAUSE
MediaControlIntent.ACTION_RESUME
MediaControlIntent.ACTION_STOP
MediaControlIntent.ACTION_GET_STATUS
MediaControlIntent.ACTION_SEEK
These are placed on an independent IntentFilter
because, technically, we can support
these actions on any type of media. In the case of this specific example, the only media
we support is streaming video. But, we could configure other IntentFilter
objects, like
ifPlay
was, stating yet other media types that we handle.
To fully comply with the RemotePlaybackClient
API, we must advertise that we handle
all of those actions… even if our intended client will not use all of them.
We could also:
MediaControlIntent.ACTION_START_SESSION
MediaControlIntent.ACTION_ENQUEUE
) and manipulating that queue
(e.g., MediaControlIntent.ACTION_REMOVE
)All of those are demonstrated in Google’s sample app.
Just because we have some static IntentFilter
objects does not mean that anything will
pay attention to them. We need to actually register them with the media routing framework,
wrapped in a pair of “descriptor” objects. DemoRouteProvider
calls a private
handleDiscovery()
method from the constructor, where handleDiscovery()
sets up
the descriptors:
private void handleDiscovery() {
MediaRouteDescriptor.Builder mrdBuilder=
new MediaRouteDescriptor.Builder(DEMO_ROUTE_ID, "Demo Route");
mrdBuilder.setDescription("The description of a demo route")
.addControlFilter(ifPlay)
.addControlFilter(ifControl)
.setPlaybackStream(AudioManager.STREAM_MUSIC)
.setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE)
.setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
MediaRouteProviderDescriptor.Builder mrpdBuilder=
new MediaRouteProviderDescriptor.Builder();
mrpdBuilder.addRoute(mrdBuilder.build());
setDescriptor(mrpdBuilder.build());
}
In the end, we need to provide a MediaRouteProviderDescriptor
to the MediaRouteProvider
by means of a setDescriptor()
method. MediaRouteProviderDescriptor
is, in effect,
metadata about the MediaRouteProvider
itself. At the present time, the only thing this
holds is a set of MediaRouteDescriptor
objects, one for each media route that the
MediaRouteProvider
claims to support.
A MediaRouteProvider
is made up of several pieces of information, including:
IntentFilter
(s) representing the supported actions and, where relevant, MIME types
and schemesMediaRouteActionProvider
)These are all configured on a MediaRouteProvider
by creating a MediaRouteProvider.Builder
and supplying the values either in the Builder
constructor or via fluent setter methods.
In the particular case of our simple demo provider, we:
IntentFilter
objects defined earlier to indicate what actions we can
performSTREAM_MUSIC
, that the playback type
is PLAYBACK_TYPE_REMOTE
, and that the volume handling is PLAYBACK_VOLUME_FIXED
(i.e., volume should be managed on the TV or whatever the media is being played upon)It is very likely that you will elect to have several MediaRouteDescriptor
objects
for different client application scenarios. Google’s sample app uses a total of four
MediaRouteDescriptor
objects:
PLAYBACK_VOLUME_VARIABLE
(so volume is controllable by a client app)Now, we have told the media routing framework what actions we support. Some app will then
try to use RemotePlaybackClient
and ask us to perform those actions. Hence, we need to
find out when this happens, so we can do the actual work of having the playback device
actually play back the media, pause the media, etc.
To do this, we need to create a custom subclass of MediaRouteProvider.RouteController
.
This contains a series of callback methods which we can override to find out when various
events occur.
There are four such callback methods that the DemoRouteController
subclass of
MediaRouteProvider.RouteController
implements:
onSelect()
, which will be called when a client app has selected our
MediaRouteProvider
to handle some media on behalf of that client apponUnselect()
and onRelease()
, which will be called when the client app
disconnects from our MediaRouteProvider
onControlRequest()
, which will be called when some specific action that we advertised
is requested, such as playing back a piece of mediaThe DemoRouteController
just logs a message to Logcat for the first three callbacks:
@Override
public void onRelease() {
Log.d(getClass().getSimpleName(), "released");
}
@Override
public void onSelect() {
Log.d(getClass().getSimpleName(), "selected");
}
@Override
public void onUnselect() {
Log.d(getClass().getSimpleName(), "unselected");
}
The onControlRequest()
method is a bit more complex, as all control requests route
through here: play, pause, resume, stop, etc. onControlRequest()
is passed the Intent
identifying the particular action that should be performed, and we can examine the Intent
action string to determine what needs to be done. In this case, onControlRequest()
delegates
the real work to action-specific methods like onPlayRequest()
:
@Override
public boolean onControlRequest(Intent i, ControlRequestCallback cb) {
if (i.hasCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
if (MediaControlIntent.ACTION_PLAY.equals(i.getAction())) {
return(onPlayRequest(i, cb));
}
else if (MediaControlIntent.ACTION_PAUSE.equals(i.getAction())) {
return(onPauseRequest(i, cb));
}
else if (MediaControlIntent.ACTION_RESUME.equals(i.getAction())) {
return(onResumeRequest(i, cb));
}
else if (MediaControlIntent.ACTION_STOP.equals(i.getAction())) {
return(onStopRequest(i, cb));
}
else if (MediaControlIntent.ACTION_GET_STATUS.equals(i.getAction())) {
return(onGetStatusRequest(i, cb));
}
else if (MediaControlIntent.ACTION_SEEK.equals(i.getAction())) {
return(onSeekRequest(i, cb));
}
}
Log.w(getClass().getSimpleName(), "unexpected control request"
+ i.toString());
return(false);
}
onControlRequest()
should return true
if we agree to perform the action and will
use the supplied ControlRequestCallback
object to asynchronously deliver our results.
If onControlRequest()
returns false
, that means that we are rejecting the action
for some reason, such as it being one that is unrecognized. In DemoRouteController
,
that will occur if the category or the action on the Intent
is not one of the supported options.
Note that if you opted into variable volume, there are onSetVolume()
and onUpdateVolume()
callback methods that will give you access to those events.
For those actions that you advertise and receive in onControlRequest()
, you need
to actually do the work for those actions. The details of this will vary widely depending
upon your playback device and playback app that you are supporting. For example, you might
establish a WiFi Direct connection in onSelect()
, then use that connection in handling
play, pause, etc. actions.
However, a few aspects of handling these actions will be in common across all implementations:
onControlRequest()
must return true
or false
as was described in the preceding
sectiononResult()
or onError()
on the ControlRequestCallback
object to
indicate if the action succeeded or failedBundle
to those methods, particularly to onResult()
,
containing the right set of values to provide more details about the results of the actionThe details of what that Bundle
must contain are documented on the MediaControlIntent
class, on the definition of each action string (e.g., ACTION_PLAY
).
With that in mind, let’s look at the six actions supported by DemoRouteController
.
The Bundle
passed to onResult()
of the ControlRequestCallback
, when the action
is ACTION_PLAY
, needs three values:
EXTRA_SESSION_ID
: if you are implementing session management, this will be the
unique session ID (String
) for the session you are playing the media in. If you are not
implementing session management, then what you are supposed to return is undocumented
and (hopefully) unusedEXTRA_ITEM_ID
: if you are implementing “enqueue” support, this will be the
item ID (String
) for managing this item in the queue of available items. If you are not supporting
a playback queue, then what you are supposed to return is undocumented
and (hopefully) unusedEXTRA_ITEM_STATUS
: this should point to a Bundle
created from a MediaItemStatus
object where you indicate what the status is of the playback of this itemYou create a MediaItemStatus
object via a MediaItemStatus.Builder
, where you can
pass into the constructor a value indicating the overall status
(e.g., MediaItemStatus.PLAYBACK_STATE_PLAYING
), plus use fluent setter methods to
define additional characteristics of the status, such as the current seek position.
The DemoRouteController
logic for ACTION_PLAY
, in the onPlayRequest()
method,
logs the event to Logcat and crafts a valid-but-meaningless result Bundle
for use
with onResult()
:
private boolean onPlayRequest(Intent i, ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "play: "
+ i.getData().toString());
MediaItemStatus.Builder statusBuilder=
new MediaItemStatus.Builder(
MediaItemStatus.PLAYBACK_STATE_PLAYING);
Bundle b=new Bundle();
b.putString(MediaControlIntent.EXTRA_SESSION_ID, DemoRouteProvider.DEMO_SESSION_ID);
b.putString(MediaControlIntent.EXTRA_ITEM_ID, DemoRouteProvider.DEMO_ITEM_ID);
b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
statusBuilder.build().asBundle());
cb.onResult(b);
return(true);
}
The Bundle
passed to onResult()
of the ControlRequestCallback
, when the action
is ACTION_PAUSE
, ACTION_RESUME
, or ACTION_STOP
, does not need any particular values
at the present time. Hence, the DemoRouteController
methods for those actions just
log the event to Logcat and pass an empty Bundle
to onResult()
:
private boolean onPauseRequest(Intent i, ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "pause");
cb.onResult(new Bundle());
return(true);
}
private boolean onResumeRequest(Intent i, ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "resume");
cb.onResult(new Bundle());
return(true);
}
private boolean onStopRequest(Intent i, ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "stop");
cb.onResult(new Bundle());
return(true);
}
The Bundle
passed to onResult()
of the ControlRequestCallback
, when the action
is ACTION_GET_STATUS
or ACTION_SEEK
, must contain the same sort of MediaItemStatus
-built
nested Bundle
representing the current status. For ACTION_GET_STATUS
, the only “work” to
be done is to pass back the status; for ACTION_SEEK
, you should move the playback position
to the location indicated by an extra on the Intent
, then return the revised status.
In the case of DemoRouteController
, both just log a message to Logcat and return a
fairly pointless status:
private boolean onGetStatusRequest(Intent i,
ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "get-status");
MediaItemStatus.Builder statusBuilder=
new MediaItemStatus.Builder(
MediaItemStatus.PLAYBACK_STATE_PLAYING);
Bundle b=new Bundle();
b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
statusBuilder.build().asBundle());
cb.onResult(b);
return(true);
}
private boolean onSeekRequest(Intent i, ControlRequestCallback cb) {
Log.d(getClass().getSimpleName(), "seek");
MediaItemStatus.Builder statusBuilder=
new MediaItemStatus.Builder(
MediaItemStatus.PLAYBACK_STATE_PLAYING);
Bundle b=new Bundle();
b.putBundle(MediaControlIntent.EXTRA_ITEM_STATUS,
statusBuilder.build().asBundle());
cb.onResult(b);
return(true);
}
While we have defined our RouteController
, we still need to teach our MediaRouteProvider
about it. That is through overriding the onCreateRouteController()
method and returning
an instance of RouteController
:
@Override
public RouteController onCreateRouteController(String routeId) {
return(new DemoRouteController());
}
onCreateRouteController()
is passed the route ID String
used in the MediaRouteDescriptor
.
You can either use that to instantiate a different RouteProvider
, pass the String
into
a common RouteProvider
so it knows what to do, or ignore it entirely if you have only
one published route. In the case of DemoRouteProvider
, we ignore the route ID and always
return a DemoRouteController
.
DemoRouteProvider
is always available, largely because it does not do much of anything.
In the real world, your MediaRouteProvider
may not always be relevant. For example, the
TV you are set up to talk to may be powered down. Or, the user may not be at home where
the TV is, so the client device and the TV are not on the same network.
Rather than constantly polling the outside world to see if a route is possible, we only
do this when a client app requests “route discovery”, such as by providing
the MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY
flag on an addCallback()
call to
a MediaRouter
. That in turn triggers an onDiscoveryRequestChanged()
call on our
MediaRouteProvider
.
There, and in our constructor-triggered setup, we should do work to determine if a route is currently possible and set up our descriptors. This work should be done in a background thread if it involves network I/O.
Note that onDiscoveryRequestChanged()
is passed a MediaRouteDiscoveryRequest
object,
describing what the consuming app is looking for. If the request is irrelevant for your
provider (e.g., the app wants a local audio route, and you provide remote playback routes),
simply ignore it.
The onDiscoveryRequestChanged()
implementation in DemoRouteProvider
just calls
the same handleDiscovery()
method that the constructor does.
Having a MediaRouteProvider
is nice, but it is useless if apps are not going to know
about it.
You have two main options for consuming the MediaRouteProvider
: use it only within your
own app, or publish it to all apps on the device.
Using a MediaRouteProvider
for your own app is very simple. Just add a single call
to addProvider()
on your MediaRouter
, supplying an instance of your MediaRouteProvider
.
Since our sample project is a fork of the original RemotePlaybackClient
sample, we still
have a PlaybackFragment
that sets up the MediaRouter
and MediaRouteActionProvider
.
In onAttach()
of that PlaybackFragment
, we can configure our MediaRouterProvider
after obtaining the MediaRouter
instance:
@Override
public void onAttach(Activity host) {
super.onAttach(host);
router=MediaRouter.getInstance(host);
provider=new DemoRouteProvider(getActivity());
router.addProvider(provider);
}
At this point, our DemoRouteProvider
will be available as an option for the user,
along with any other eligible media routes:
Figure 818: MediaRouteProvider Demo, on a Nexus 4, Showing Available Routes
Choosing the DemoRouteProvider
(“Demo Route” in the screenshot) will allow you to use
it just like you do a Chromecast… if you do not mind the fact that nothing shows up on
your television:
Figure 819: MediaRouteProvider Demo, on a Nexus 4, After Several Commands
As it turns out, the DemoRouteProvider
works better than Google’s own MediaRouteProvider
for the Chromecast, insofar as more of the callbacks work. Specifically, we actually
receive callbacks for pause, resume, and stop events, as opposed to having to just assume
that those events completed.
Also, we remove the demo provider in onDetach()
:
@Override
public void onDetach() {
router.removeProvider(provider);
super.onDetach();
}
Among other things, this allows us to correctly handle configuration changes — if we fail
to call removeProvider()
and blindly add another provider in onAttach()
, we wind up
with multiple providers, because our MediaRouter
is a framework-provided singleton
and is not re-created with the new fragment.
If you want your MediaRouteProvider
to be used by other apps, you will need to create
one more Java class: a subclass of MediaRouteProviderService
. This requires only one
method, onCreateMediaRouteProvider()
, where you return an instance of your MediaRouteProvider
:
package com.commonsware.android.mrp;
import android.support.v7.media.MediaRouteProvider;
import android.support.v7.media.MediaRouteProviderService;
public class DemoRouteProviderService extends MediaRouteProviderService {
@Override
public MediaRouteProvider onCreateMediaRouteProvider() {
return(new DemoRouteProvider(this));
}
}
This also needs to be added to your manifest, like any other Service
. Give it an
<intent-filter>
looking for the android.media.MediaRouteProviderService
action, so the
media routing framework knows that it can obtain a MediaRouteProvider
from it:
<service
android:name="DemoRouteProviderService"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.media.MediaRouteProviderService"/>
</intent-filter>
</service>
However, do not do both addProvider()
and have the <service>
element. If you use
the <service>
element, your app can use the MediaRouteProvider
, just as can any other
app on the device. Hence, in the published source code for this sample, the <service>
element is commented out — you will need to uncomment it, and comment out the addProvider()
call, to test the DemoRouteProvider
with other apps.
Of course, DemoRouteProvider
is just a demo and does not actually play any media anywhere.
It is here to give you the basic steps for responding to RemotePlaybackClient
requests.
For a production MediaRouteProvider
, in addition to the usual tightening-up of the code
(e.g., better exception handling), you will need to work on other areas as well, ones that
are beyond the scope of the sample app.
Of course, the big one is passing the actions over to the playback device, so you actually do play back media.
If you are the developer of the playback device and its protocols (e.g., it is an Android device, and you are writing the playback app for it), then you can choose how you wish to handle the communications. You can work with low-level socket protocols directly, or you can leverage libraries like AllJoyn or ZeroMQ.
If the playback device “is what it is”, and you cannot change it, then you will need
to determine what protocols it offers and how best to map the MediaControlIntent
actions to that protocol.
Also note that onControlRequest()
is designed for asynchronous operation. The
sample app just invoked the ControlRequestCallback
during the onControlRequest()
processing. Usually, though, your communications with the playback device will not be
as fast as a call to Log.d()
. You should arrange to do those communications in
a background thread, perhaps via a single-thread thread pool as an ExecutorService
.
Simply pass the ControlRequestCallback
to that thread along with the rest of the
action’s data (e.g., the URL of the media to load), and the thread can call onResult()
or onError()
as needed.
As was noted in the description of the sample app, that app avoids:
Any of those may be of interest to your users, and so you may need to consider offering them at some point. Also note that some potential client apps might need those capabilities and therefore will not see or use your published media routes without them.
When setting up the MediaRouteProvider
, we create one or more MediaRouteDescriptor
objects wrapped around one or more IntentFilter
objects. Those IntentFilter
objects indicate what actions we support. The DemoRouteProvider
uses standard
actions (e.g., ACTION_PLAY
) in a standard category (CATEGORY_REMOTE_PLAYBACK
).
However, you are not limited to that.
You are welcome to also support custom actions in a custom category, to represent
other things that your particular MediaRouteProvider
offers. You can then
use those actions from your own client app, or document them for use by third-party
apps.
The client app can use supportsControlRequest()
and sendControlRequest()
to determine whether a particular media route supports a particular Intent
that represents an action to be performed by that route’s MediaRouteProvider
.
This way, a client app can work both with your custom MediaRouteProvider
(taking advantage of your custom actions) and with regular providers that lack
such support, assuming that the client can gracefully degrade its functionality.
Google’s sample app defines a custom ACTION_GET_STATISTICS
action that
their sample client requests where available and their sample provider implements.