A term that you will encounter a fair bit as an Android developer is “Google Play Services”, or “Play Services” for short. This is your gateway into a series of proprietary capabilities that Google has layered on top of Android. Many of these capabilities are tied to Google’s servers and services, such as ads and Google Drive.
However, these capabilities, while usually free from monetary cost to the developer, are not free from problems or controversy.
Google Play Services is a “kitchen sink” term, encompassing a wide range of things from the standpoint of developers and users alike.
The Play Services SDK allows you to integrate your Android app with a number of Google proprietary services, from leaderboard management for games to interacting with Chromecast devices. Many, but not all, of these services are tied to Google servers. Many, but not all, of these services will require some sort of API key as a result.
The SDK comes in the form of an Android library project that you link into your app, giving you access to classes and methods that let you add maps, or payment options, or push message receipt into your Android apps.
Note that while the name “Play Services” contains the word “services”, Play Services is merely an API, one that does not directly have anything to do with services or system services.
In Western countries, the common perception is that all Android devices are part of the Google Play world. These devices will have the Play Services Framework pre-installed from the device manufacturer and silently updated over the air by Google. Apps that use the Play Services SDK in theory can use all of the SDK’s available APIs on all devices equipped with the Play Services Framework.
In practice, older devices (particularly Android 2.x) will have some
number of limitations related to Play Services, not the least of which
being the lack of automatic over-the-air updates. As many developers
are now setting their minSdkVersion
to be something newer (e.g., 15),
this particular class of problems will tend to fall by the wayside.
Google’s continued expansion of the Play Services SDK, sometimes at the expense of Android itself, has not proven to be universally popular:
As mentioned earlier, the Play Services SDK is vast. The following sub-sections outline some of the major pieces of the Play Services SDK, what Gradle dependency pulls them in, and what independent alternatives exist (if any).
Google has tried a couple of times to get into the mobile payments market, starting with Google Wallet, which has now morphed into Android Pay. If you want to allow users to purchase goods and services through your app, and you want to allow those users to pay via Android Pay, you can use this portion of the Play Services SDK.
To communicate from a device running an open source operating system (Android, on a phone or tablet) to a device running an open source operating system (Android, on an Wear OS device), you have to use a proprietary, closed-source library.
It is possible to show a Notification
on a Wear device
straight from the Android SDK. It is also possible to create
a Wear app that exists standalone straight from the Android SDK.
But if you want to send data to the Wear device from the phone or
tablet, or vice versa, that requires the Wear portion of the
Play Services SDK.
This library provides a few discrete APIs for communication:
The documentation and business proposition for the Google+ API is a bit limited at this time. However, it appears that you can:
ACTION_SEND
Rather than maintain your own account system, your app could ask the users to sign into their Google account as part of using your app.
“Analytics” refers to tracking usage. Web analytics uses a mix of Web server logs, tracking cookies, and the like to determine popular Web pages, navigation flows, time spent in certain areas of a site, and so forth. Mobile analytics tracks usage within an app: certain activities, certain operations, etc.
Google Analytics is very popular for Web sites, and Google extended this to a mobile API designed for tracking app usage.
There are countless analytics services with Android APIs (e.g., Flurry) beyond Google’s. While there appear to be few self-hosted or open source solutions, analytics data collection is not especially difficult to implement on your own, if you would prefer to keep this information more private. Data analysis is where the challenges with home-grown solutions arise. Or, you could simply not collect this sort of information.
Google App Indexing, among other things, allows for “deep links” into an Android app, surfaced from Google search results. That, on its own, does not require any particular proprietary APIs. However, to allow Google to discover these “deep links”, it appears that you need to use a custom app-indexing API.
Google’s App Invites service allows your users to annoy their contacts, bugging them to install your app.
A simpler, albeit less slick, solution is to allow the user to send
messages from your app with a link back to your app from its distribution
channel (e.g., Play Store), such as via an ACTION_SEND
Intent
.
Google Cast can be thought of as a control protocol for Google Cast-enabled receivers. Through a Google-supplied SDK (or other means), Google Cast client apps (“senders”) can direct a Google Cast-enabled receiver to play, pause, rewind, fast-forward, etc. a stream. Android TV devices and Chromecast devices are the primary Cast-enabled receivers.
Google Cast does assume that, in general, the media receiver runs its own OS and is capable of playing streaming media without ongoing assistance from the Google Cast client. Hence, the client is not “locked into” having to keep feeding content to the Google Cast client, allowing the user to go off and do other things with that client while playback is going on.
Chromecast offers up remote playback media routes and works with RemotePlaybackClient
,
as is discussed in the chapter on MediaRouter
. The sample app
for RemotePlaybackClient
was tested on a Chromecast.
If you want greater control than is offered via RemotePlaybackClient
, though,
you can use the Cast SDK. However,
using the Cast SDK will tie you to Google Cast — and some of its restrictions,
both technical and legal — but will give you greater developer control over the
behavior of both the Google Cast device and your app.
As noted above, RemotePlaybackClient
, along with
the Presentation
API, offer a significant subset of
what the Cast SDK offers.
Google Cloud Messaging – GCM for short — asynchronously delivers notifications from the Internet (“cloud”) to Android devices. Rather than the device waking up and polling on a regular basis at the behest of your app, your app can register for notifications and then wait for them to arrive. GCM is engineered with efficiency in mind:
The proper use of GCM means better battery life for your users. It can also reduce the amount of time your code runs, which helps you stay out of sight of users looking to pounce on background tasks and eradicate them with task killers.
GCM has gone through four revisions of its API, including the 2016 rebranding of it as Firebase Cloud Messaging (FCM). Be sure to use up-to-date references and examples when adding GCM/FCM to your apps.
You may also encounter references to “C2DM”, GCM’s precursor. C2DM debuted in 2010 and quickly became popular, for everything from triggering near-real-time data synchronization (e.g., Remember the Milk to-do list updates) to lightweight coordination between multiple players in a game. However, C2DM was a Google Labs product and in perpetual beta form. When Google Labs was shut down, C2DM was in limbo: not canceled, but not converted into an actual product. In 2012, GCM formally replaced C2DM, and in 2015, C2DM was shut down entirely. Hence, while high-level concepts about push messaging from the C2DM era might still be relevant to you, any actual C2DM-related code will be useless.
Other devices from outside the Google Play ecosystem may offer their own counterparts to GCM. Independent push implementations can range from XMPP and MQTT to simple WebSockets, though these have limitations when compared to GCM.
Google Drive is Google’s hosted file-storage service. Via Drive APIs in the Play Services SDK, you can work indirectly with the user’s Google Drive-hosted content, including creating and deleting files, plus searching through files for ones that meet particular search criteria.
Note that some of this functionality is available via the Storage Access Framework in Android 4.4+, with the advantage that it works across multiple content sources, not just Google Drive.
Other services (e.g., Dropbox) have their own APIs as well.
Google Fit is Google’s wearable sensor initiative, for “smartbands” and related gadgets. Through the Fit APIs, you can detect Fit gadgets associated with a user’s device, read data from those gadgets’ sensors (e.g., heart rate), and so forth.
Other manufacturers in this space (e.g., Fitbit) have their own SDKs as well.
This portion of the Play Services SDK offers the “fused location provider”. This combines GPS and network sources of location data, plus sensor information, to try to offer better location information with less power draw. For example, if the sensors suggest that the device is not moving, the fused location provider can scale back how aggressively it uses the location sources, since the location probably is not changing.
This library also offers a “geofencing” implementation, where you ask the Play Services SDK to keep track of certain locations and let you know if the device gets within a certain distance of those locations.
This book has a chapter on the fused location provider.
Android has offered integrated Google Maps to developers since the outset. With the introduction of Maps V2 in 2012, this capability was folded into the Play Services SDK. Through Maps V2, you can embed a map powered by Google Maps into your application, complete with markers and popups, lines and shaded areas, and so on.
This book has a chapter on Maps V2.
Due to the popularity of embedded maps, other manufacturers (e.g., Amazon, Blackberry) have offered their own map engines, often with APIs that attempt to mimic that of Maps V2 (or perhaps its predecessor, now known as Maps V1). Beyond that, there is the OpenStreetMap project, for which Android libraries are available.
Google is an advertising company. They offer the Google Mobile Ads SDK (a.k.a., AdMob for Android) as part of the Play Services SDK, for you to be able to add banners, interstitials, and other forms of advertising to your app.
There are other competing mobile ad networks that you could consider, though you may be better served focusing on coming up with a better business model.
Google has a variety of APIs, grouped under the “Mobile Vision” banner, designed for detecting specific sorts of objects or other information in still photos and videos. These include:
Android’s native camera API has some amount of face recognition, though not to the level of the Face API in the Mobile Vision SDK.
There are a variety of barcode scanning apps (e.g., the legendary ZXing Barcode Scanner) and libraries (e.g., ZBar) that one can use independently of the Play Services SDK.
Google Nearby offers a pair of APIs for communication between nearby devices.
The Nearby Messages API offers a publish-and-subscribe messaging framework, designed for sending small blocks of data between Internet-connected Android and iOS devices. This is largely frictionless for the user (beyond the network connection), as the Messages API uses a mix of radios (Bluetooth, Bluetooth LE, WiFi) and ultrasonic signaling to handle the pairing and interaction.
The Nearby Connections API offers connection-based group messaging between devices on the same WiFi network. While you can pass more data this way, since everybody has to be on WiFi, it reduces the number of potential communications partners.
While some aspects of Google Nearby (e.g., ultrasound) are unusual, there have been many projects offering server-less group communications, from ZeroMQ to AllJoyn.
The SafetyNet APIs lets your app know “whether the device where it is running matches the profile of a device that has passed Android compatibility testing”. Presumably, this is designed to help you detect custom ROMs or copies of your app installed from pirate sites onto incompatible hardware.
On the surface, using Play Services should be simple: add the
aforementioned implementation
statement(s), then start calling some
methods from the supplied Play Services SDK libraries.
Unfortunately, it is not that simple. There are a number of other things that you will need to deal with in order to integrate Play Services into your app.
You will see plenty of examples that show having a <meta-data>
element,
inside your <application>
element, with an android:name
of
com.google.android.gms.version
and a value pulling in an integer
resource (@integer/google_play_services_version
) from the Play Services
SDK:
<application
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Apptheme">
<activity android:name=".WeatherDemo">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".LegalNoticesActivity"/>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version"/>
</application>
This is no longer required, if you are using version 8.1.0 or higher of the Play Services SDK. This element will be added to your manifest automatically via the manifest merger process.
Android 6.0’s runtime permission system affects some of the Play Services
APIs. For example, if you are trying to get the location via the
fused location provider, you will need ACCESS_COARSE_LOCATION
or ACCESS_FINE_LOCATION
. Both of those are dangerous
permissions,
so apps with a targetSdkVersion
of 23 or higher will need to request
those permissions at runtime.
The
Location/FusedNew
sample application contains an AbstractGoogleApiClientActivity
that, among
other things, helps us deal with runtime permissions in our Play Services
SDK-using apps.
The idea behind AbstractGoogleApiClientActivity
is that apps using
the Play Services SDK will have activities that inherit from
AbstractGoogleApiClientActivity
, overriding a few methods to configure
how AbstractGoogleApiClientActivity
handles things like runtime permissions.
For example, AbstractGoogleApiClientActivity
has an abstract
method named getDesiredPermissions()
that subclasses must override, providing a String
array of permissions
that the activity needs. AbstractGoogleApiClientActivity
then uses
hasAllPermissions()
and hasPermission()
private methods to determine
whether all of the requested permissions are currently held:
private boolean hasAllPermissions(String[] perms) {
for (String perm : perms) {
if (!hasPermission(perm)) {
return(false);
}
}
return(true);
}
private boolean hasPermission(String perm) {
return(ContextCompat.checkSelfPermission(this, perm)==
PackageManager.PERMISSION_GRANTED);
}
In onCreate()
of AbstractGoogleApiClientActivity
, among other things,
we call hasAllPermissions()
to see if we have all of our required
permissions — if yes, we can go ahead and call an initPlayServices()
method to start the process of initializing our access to the Play Services
SDK:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState!=null) {
isInPermission=
savedInstanceState.getBoolean(STATE_IN_PERMISSION, false);
isResolvingPlayServicesError=
savedInstanceState.getBoolean(STATE_IN_RESOLUTION, false);
}
if (hasAllPermissions(getDesiredPermissions())) {
initPlayServices();
}
else if (!isInPermission) {
isInPermission=true;
ActivityCompat
.requestPermissions(this,
netPermissions(getDesiredPermissions()),
REQUEST_PERMISSION);
}
If we do not have all of the permissions, onCreate()
will call
requestPermissions()
on ActivityCompat
to ask the user for them.
However, it also leverages netPermissions()
to filter out the permissions
that the user previously granted, so we only bother the user with
permissions that either the user has not seen before or has previously
denied:
private String[] netPermissions(String[] wanted) {
ArrayList<String> result=new ArrayList<String>();
for (String perm : wanted) {
if (!hasPermission(perm)) {
result.add(perm);
}
}
return(result.toArray(new String[result.size()]));
}
Note that this code is not making use of shouldShowRequestPermissionRationale()
,
to detect previous permission denials and perhaps show some UI to educate
the user on what the impacts are of this rejection.
The call to requestPermissions()
will eventually trigger a callback
to onRequestPermissionsResult()
. Here, if we now have all of the
permissions, we call initPlayServices()
(more on this in a bit)
and then connect()
to the Play Services SDK (also, more on this in
a bit):
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions,
int[] grantResults) {
isInPermission=false;
if (requestCode==REQUEST_PERMISSION) {
if (hasAllPermissions(getDesiredPermissions())) {
initPlayServices();
playServices.connect();
}
else {
handlePermissionDenied();
}
}
}
If, however, we do not have all of the requested permissions,
another abstract
method on this class is handlePermissionDenied()
,
where the subclass can do what it wants to. That could range from
explaining to the user what can and cannot be done to simply calling
finish()
and going away.
There is a possibility that the user will rotate the screen or otherwise
trigger a configuration change while we are in the request-permission
process. Even though our activity is not in the foreground from an
input standpoint, it is visible, and so it will undergo the configuration
change while the request-permission dialog is still in the foreground.
We do not want to pop up the dialog again (and confuse the user).
So, the isInPermission
field is tracking whether the request-permission
dialog is outstanding, so we do not attempt to show the dialog again
in onCreate()
.
Since the activity could be destroyed and recreated as part of the
configuration change, we hang onto the isInPermission
value in the
saved instance state Bundle
:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_IN_PERMISSION, isInPermission);
outState.putBoolean(STATE_IN_RESOLUTION,
isResolvingPlayServicesError);
}
(the STATE_IN_RESOLUTION
bit will be explained shortly)
And, in onCreate()
, we re-initialize isInPermission
if we got
the saved instance state Bundle
passed in.
While you typically think of Android devices as having Play Services, that is not always the case. Sometimes, they do not, yet wind up having a copy of your app anyway, perhaps through less-than-legal measures. Or, the device has Play Services, but it is not the latest version — perhaps the user missed a recent Play Services update due to international travel, taking up temporary residence in a Faraday cage, or other technical issues.
Hence, another thing that AbstractGoogleApiClientActivity
does is
confirm that the device has Play Services and can connect to the Play
Services process for whatever particular API(s) we wish to use.
For many, though not all, Play Services APIs, you use a GoogleApiClient
as your entry point for talking to Play Services. Some APIs, like
Maps V2, do not use GoogleApiClient
for some reason. But,
more often than not, you will find yourself needing GoogleApiClient
.
To create a GoogleApiClient
instance, use a GoogleApiClient.Builder
. As the
class name suggests, GoogleApiClient
is used as a client connection to many
(but not all) Google Play Services APIs, and GoogleApiClient.Builder
is a
builder for building such a connection. In particular:
Activity
as our Context
addConnectionCallbacks()
to indicate what should
be notified when our connection to the Play Services process
is readyaddOnConnectionFailedListener()
to indicate what should
be notified if we have a problem connecting to the
Play Services processbuild()
on the Builder
to actually build the GoogleApiClient
This is handled by initPlayServices()
on AbstractGoogleApiClientActivity
,
which we call once we have our permissions set up:
protected void initPlayServices() {
playServices=
configureApiClientBuilder(new GoogleApiClient.Builder(this))
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
This includes calling out to the subclass’ implementation of
configureApiClientBuilder()
, where the subclass can use methods like
addApi()
to indicate specifically what parts of the Play Services
family of APIs the activity wants to use.
Given that we have a GoogleApiClient
, we need to connect()
to it to be able to start
requesting location data, then disconnect()
from it when we no longer need that
location data.
Disconnecting is easy: we do that in onStop()
of AbstractGoogleApiClientActivity
:
@Override
protected void onStop() {
if (playServices!=null) {
playServices.disconnect();
}
super.onStop();
}
There are two places where we possibly call connect()
. One is if
we needed to ask for permissions, and the user granted them.
In onRequestPermissionsResult()
, after confirming that we do indeed
have all necessary permissions, we call initPlayServices()
and then
immediately call connect()
on the GoogleApiClient
:
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions,
int[] grantResults) {
isInPermission=false;
if (requestCode==REQUEST_PERMISSION) {
if (hasAllPermissions(getDesiredPermissions())) {
initPlayServices();
playServices.connect();
}
else {
handlePermissionDenied();
}
}
}
If we did not need to request permissions, we call connect()
in
onStart()
, mirroring the onStop()
where we are disconnecting:
@Override
protected void onStart() {
super.onStart();
if (!isResolvingPlayServicesError && playServices!=null) {
playServices.connect();
}
}
The isResolvingPlayServicesError
boolean
value will be discussed a
bit later in this chapter.
The call to connect()
, in turn, will trigger calls to our onConnected()
and onDisconnected()
methods
of the GoogleApiClient.ConnectionCallbacks
interface, assuming all goes well.
AbstractGoogleApiClientActivity
does not provide those implementations;
they are considered part of the abstract
API and therefore need to be
implemented by subclasses.
However, apparently it is possible for this connection attempt to fail. Exactly how and
why it might fail is not well documented. If it fails, the onConnectionFailed()
method from our GoogleApiClient.OnConnectionFailedListener
implementation will
be called. onConnectionFailed()
is passed a ConnectionResult
indicating what
specifically went wrong.
It turns out that this ConnectionResult
may contain a PendingIntent
that can be
used to try to help the user recover from whatever the problem was. The recipe
that we have been given
to try to use this is to call hasResolution()
(to see if the PendingIntent
exists)
and to use startResolutionForResult()
(to invoke the activity pointed to by the
PendingIntent
). Of course, hasResolution()
may return false
, and apparently
the PendingIntent
might be broken, so we have to handle those scenarios as well:
@Override
public void onConnectionFailed(ConnectionResult result) {
if (!isResolvingPlayServicesError) {
if (result.hasResolution()) {
try {
isResolvingPlayServicesError=true;
result.startResolutionForResult(this, REQUEST_RESOLUTION);
}
catch (IntentSender.SendIntentException e) {
playServices.connect();
}
}
else {
ErrorDialogFragment.newInstance(result.getErrorCode())
.show(getFragmentManager(),
TAG_ERROR_DIALOG_FRAGMENT);
isResolvingPlayServicesError=true;
}
}
}
If we have a resolution and successfully start up the resolution activity, our
activity will be stopped and later started, at which point we will wind up trying
to connect()
again naturally.
If there is no PendingIntent
to try to resolve the problem, we can still
attempt to display a dialog with information about what is going wrong.
The Play Services SDK provides this dialog, though we are responsible
for wrapping it in a DialogFragment
ourselves. That comes in the form
of ErrorDialogFragment
:
public static class ErrorDialogFragment extends
DialogFragment {
static final String ARG_ERROR_CODE="errorCode";
static ErrorDialogFragment newInstance(int errorCode) {
Bundle args=new Bundle();
ErrorDialogFragment result=new ErrorDialogFragment();
args.putInt(ARG_ERROR_CODE, errorCode);
result.setArguments(args);
return(result);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return(GoogleApiAvailability
.getInstance()
.getErrorDialog(
getActivity(),
getArguments().getInt(ARG_ERROR_CODE),
REQUEST_RESOLUTION));
}
@Override
public void onCancel(DialogInterface dlg) {
if (getActivity()!=null) {
getActivity().finish();
}
super.onCancel(dlg);
}
@Override
public void onDismiss(DialogInterface dlg) {
if (getActivity()!=null) {
((AbstractGoogleApiClientActivity)getActivity())
.isResolvingPlayServicesError=false;
}
super.onDismiss(dlg);
}
}
onCreateDialog()
uses the GoogleApiAvailability
singleton to
show the error dialog, given the error code that came from our
previous attempt to connect. We pass that error code over to the
ErrorDialogFragment
via the arguments Bundle
, so that it can
survive a configuration change.
However, we also have to take into account that the device might
undergo a configuration change while either the resolution activity started
by startActivityForResult()
or the ErrorFragmentDialog
is in
the foreground. What we do not want to do is immediately try
connecting to Play Services again in onStart()
, while we are in
the process of trying to fix whatever problem prevented us from connecting
to it previously.
So, we have to track a boolean
state, isResolvingPlayServicesError
,
as a field in our activity. That is initially set to false
, but
we flip
it to true
if we show the resolution activity or the
ErrorFragmentDialog
. We flip it back to false
when either the
started activity returns control to us in onActivityResult()
or
when the ErrorDialogFragment
is dismissed. While that flag is
true
, we skip attempting to connect to Play Services in onStart()
.
And this flag is part of our saved instance state, so we can handle
configuration changes.