Android Programming: Pushing the Limits (2014)
Part III. Pushing the Limits
Chapter 19. Google Play Services
Google provides a wide range of online services that can be used on websites, desktop applications, or mobile
applications like Android. In Chapter 17, I discuss how to integrate online web services with your application,
but in those examples, I use third-party services outside the Google ecosystem. In this chapter, I focus on online
services managed by Google.
You now have available a set of APIs called Google Play Services that lets you integrate Google APIs with very
little effort, and I cover a some of those services in this chapter.
The new Location API covered in Chapter 13 is also part of Google Play Services. However, as the
Location API is a bit different from the ones covered here, it deserves its own chapter.
In this chapter, you find out how to get authorization to the online services such as Google Drive, services
running on the Google Cloud Platform, and the new Google Play Games APIs. You also discover how to set up
push notifications for your application using the Google Cloud Messaging service.
You can include the Google Play Services APIs in your project by adding the following to your Gradle build-file
dependencies section:
compile ‘com.google.android.gms:play-services:3.1.36’
Authorization
Before you can start using any of the Google Play Services, or any other Google services, you need to retrieve
an authorization token for the Google account of the user. To do so, you first need to fetch the account name
for the user.
The easiest way to retrieve the account name for a user is to use the following code:
public void doConnectAccounts(MenuItem menuItem) {
Intent intent = AccountPicker.newChooseAccountIntent(null, null,
new String[] {“com.google”}, false,
“Pick one of your Google accounts to connect.”,
null, null, null);
startActivityForResult(intent, ACCOUNT_REQUEST);
}
Calling AccountPicker.newChooseAccountIntent()starts an Activity that returns the chosen
account. By passing “com.google” as one of the allowed account types, the picker will filter out all other
account types. If multiple accounts are available, the user will see a dialog box where he can select the one he
wants to use (see Figure 19-1).
Figure 19-1 The dialog box presented when there are
multiple Google accounts available
After the picker Activity returns with a result, you check whether the call was successful and retrieve the
selected account name, as shown in the following code:
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == ACCOUNT_REQUEST) {
if(resultCode == RESULT_OK) {
String accountName
= data.getStringExtra(AccountManager.KEY_ACCOUNT_
NAME);
mPrefs.edit().putBoolean(PREFS_IS_AUTHORIZED, true)
.putString(PREFS_SELECTED_ACCOUNT, accountName).apply();
Log.d(TAG, “Picked account: “ + accountName);
invalidateOptionsMenu();
authorizeAccount(accountName);
} else {
Log.e(TAG, “No account picked...”);
}
}
}
In this case, you also store the account name in a SharedPreferences along with a flag indicating that the
user successfully selected an account.
Before you request an auth token, you must decide on the scope of the permissions you need for the services
you’ll use. These scopes are standard OAuth2 scopes, and you can find them for each Google API in their
respective online documentation. In this case, you request access to the user’s App Data on Google Drive (see
the section “Google Drive Application Data” later in this chapter), the user information (such as full name), and
the user’s Google+ information. The following code shows how such a String should look.
public static final String AUTH_SCOPE =
“oauth2:https://www.googleapis.com/auth/drive.appdata “ +
“https://www.googleapis.com/auth/userinfo.profile “ +
“https://www.googleapis.com/auth/plus.me”;
After you have the account name, you can fetch an auth token using the class GoogleAuthUtil. This
operation must run off the main thread because it performs network operations, so as shown in the following
code, you wrap it in an AsyncTask:
class MyAuthTokenTask extends AsyncTask<String,Void,String> {
@Override
protected String doInBackground(String... accountName) {
String authToken = null;
try {
authToken = GoogleAuthUtil.getToken(ServicesDemo.this,
accountName[0], AUTH_SCOPE);
} catch (IOException e) {
Log.e(TAG, “Error getting auth token.”, e);
} catch (UserRecoverableAuthException e) {
Log.d(TAG, “User recoverable error.”);
cancel(true);
startActivityForResult(e.getIntent(), TOKEN_REQUEST);
} catch (GoogleAuthException e) {
Log.e(TAG, “Error getting auth token.”, e);
}
return authToken;
}
@Override
protected void onPostExecute(String result) {
// Auth token acquired – start performing API requests
if (result != null) {
mPrefs.edit().putString(PREFS_AUTH_TOKEN, result).apply();
}
}
}
This operation will always fail on the user’s first attempt (unless she authorized this application on another
device earlier), so you need to catch the UserRecoverableAuthException and call startActivityFor
Result() using the Intent contained in the exception. This brings up another dialog where the user is
informed about the API permissions your application has requested (not to be confused with standard Android
permissions). The following code shows how the onActivityResult() callback method looks in this case:
protected void onActivityResult(int requestCode, int resultCode, Intent
data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == ACCOUNT_REQUEST) { // Account picked
if(resultCode == RESULT_OK) {
String accountName
= data.getStringExtra(AccountManager.KEY_ACCOUNT_
NAME);
mPrefs.edit().putBoolean(PREFS_IS_AUTHORIZED, true)
.putString(PREFS_SELECTED_ACCOUNT, accountName).
apply();
invalidateOptionsMenu();
new MyAuthTokenTask().execute(accountName);
} else {
Log.e(TAG, “No account picked...”);
}
} else if(requestCode == TOKEN_REQUEST) { // Token requested
if(resultCode == RESULT_OK) {
// Try again...
new MyAuthTokenTask().
execute(mPrefs.getString(PREFS_SELECTED_ACCOUNT,
null));
}
}
}
Note that you modify the earlier onActivityResult() to handle recovery from errors that might
occur on the first call to GoogleAuthUtil.getToken(). All you do here is retry the execution of
MyAuthTokenTask because you should now have the right permissions.
Google Drive Application Data
Every Android user who has a Google account also has access to the Google Drive cloud storage service. This
service has a number of convenient APIs for reading and writing files and data to the user’s Google Drive
storage. With this the Google Drive API, you can store data in Google Drive that can be seen or accessed by only
one application. If you want your application to provide the capability of syncing its working data online, you’ll
find that this API is very handy.
This application data feature should not be confused with the backup features built into Android, where the
developer cannot control when files are backed up. This is also different from the Game Cloud Save API, which
is focused on saving game states across devices. You should use Google Drive and the Application Data features
for larger amounts of data that is only valid for your application—for instance, a sketching application that saves
files in a proprietary format.
As with the Location API described in Chapter 13, you need to add your application’s package name and SHA1
key string in the Google API Console, as shown in Figure 19-2. Create a new Client ID and add the information
needed. After this is done, your application (the package name you specified) will be authorized to access
Google Drive on behalf of users.
Figure 19-2 Adding a new Client ID for Google Drive API access
Before you get started with the Android code, you need to add the necessary dependencies to your Gradle
build file.
compile ‘com.google.api-client:google-api-client-android:1.16.0-rc’
compile ‘com.google.apis:google-api-services-drive:v2-rev89-1.15.0-rc’
The first line adds the generic Google Client for Android library. The second line adds the Google Drive–specific
library that you will use in this case.
Because your applications have already received an auth token from the authorization step (shown in the
previous section), you can now create a GoogleAccountCredentials that will be used by the Drive API.
The returned Drive instance will be authorized to access the user’s private App Data. The following code
shows the createDriveService() that implements this:
public Drive createDriveService(accountName) {
try {
GoogleAccountCredential googleAccountCredential =
GoogleAccountCredential.usingOAuth2(this,
Arrays.asList(DriveScopes.DRIVE_APPDATA));
googleAccountCredential.setSelectedAccountName(accountNAme);
Drive.Builder builder =
new Drive.Builder(AndroidHttp.newCompatibleTransport(),
new AndroidJsonFactory(),
googleAccountCredential);
return builder.build();
} catch (IOException e) {
Log.e(TAG, “Error”, e);
} catch (GoogleAuthException e) {
Log.e(TAG, “Error”, e);
}
return null;
}
When you want to store data that is private to your application in the App Data folder in Google Drive, you simply
create a File object (note that this is not the java.io.File class, but one specific for Google Drive API) and
populate it with the relevant metadata. You provide it with the content (in this case, a ByteArrayContent)
and insert the file into Google Drive. The result will be a file that is hidden in the user interface of Google Drive.
The following code shows how to add a new file to the App Data “folder” of Google Drive:
class MyGoogleDriveAppDataTask extends AsyncTask<JSONObject, Void,
Integer> {
@Override
protected Integer doInBackground(JSONObject... jsonObjects) {
String accountName = mPrefs.getString(PREFS_SELECTED_ACCOUNT,
null);
Drive drive = createDriveService(accountName);
int insertedFiles = 0;
for (JSONObject jsonObject : jsonObjects) {
String dataString = jsonObject.toString();
String md5 = getMD5String(dataString.getBytes());
File file = new File();
file.setTitle(md5);
String mimeType = “application/json”;
file.setMimeType(mimeType);
file.setParents(Arrays.
asList(new ParentReference().setId(“appdata”)));
ByteArrayContent content
= new ByteArrayContent(mimeType,
dataString.getBytes());
try {
drive.files().insert(file,content).execute();
insertedFiles++;
} catch (IOException e) {
Log.e(TAG, “Failed to insert file with content “
+ dataString, e);
}
}
return insertedFiles;
}
private String getMD5String(byte[] data) {
MessageDigest mdEnc = null;
try {
mdEnc = MessageDigest.getInstance(“MD5”);
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, “Error retrieving MD5 function!”, e);
return null;
}
mdEnc.update(data, 0, data.length);
return new BigInteger(1, mdEnc.digest()).toString(16);
}
}
The user can see the space used by your application, delete its content, and disconnect your app from her
Google Drive account through the Manage Apps menu in the Google Drive settings (see Figure 19-3).
Figure 19-3 The apps connected to the user’s Google Drive account
Reading and updating files is done through the same API as just shown. Note that all of the operations require
network access, so remember to perform these operations off the main thread.
Google Cloud Endpoints
Google has gathered all its cloud-based developer resources and called it Google Cloud Platform. This is basically
an improved interface for App Engine, Compute Engine, Cloud Datastore, and all the other online services that
Google provides. Most of these services have a free tier, so getting started with building a cloud backend for
your mobile application is very easy. To get started, visit https://cloud.google.com/console and
create a new project.
The easiest way to get started with your own Cloud Platform backend is to generate a basic App Engine
backend from Android Studio (see Figure 19-4).
Figure 19-4 Generating App Engine endpoints with Android Studio
After the code generation is complete, you’ll have two new modules ending with “endpoint” and “AppEngine.”
You can now start to add new POJOs (Plain Old Java Objects) with getters and setters that can be queried,
inserted, updated, and deleted from your Android application.
The following code is an example of a simple POJO for storing tasks:
@Entity
public class TaskInfo {
@Id
private String id;
private String title;
private String content;
public TaskInfo() {
}
public TaskInfo(String id, String title, String content) {
this.id = id;
this.title = title;
this.content = content;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
Following this example, you add this TaskInfo class to the AppEngine module in the project and then run
“Generate endpoint” followed by “Generate Client Libraries” from the Tools menu again. This creates the client-
side libraries that you can use in your application.
The AsyncTask shown next is an example of how to add a new TaskInfo:
class AddTaskInfo extends AsyncTask<TaskInfo, Void, Void> {
@Override
protected Void doInBackground(TaskInfo... taskInfos) {
mTaskInfoEndpoint = CloudEndpointUtils.
updateBuilder(new Taskinfoendpoint.Builder(
AndroidHttp.newCompatibleTransport(),
new JacksonFactory(),
new HttpRequestInitializer() {
public void initialize(HttpRequest
httpRequest) {
}
})
).build();
for (TaskInfo taskInfo : taskInfos) {
try {
mTaskInfoEndpoint.insertTaskInfo(taskInfo).execute();;
} catch (IOException e) {
Log.e(TAG, “Error inserting task.”, e);
}
}
return null;
}
}
Note: Because they will perform network calls that would otherwise block the main thread, all these calls need
to happen on a dedicated thread.
The following shows how to use the same endpoint to query for all TaskInfo instances stored in your App
Engine instance:
class GetAllTaskInfo extends AsyncTask<Void, Void, List<TaskInfo>> {
@Override
protected List<TaskInfo> doInBackground(Void... voids) {
try {
mTaskInfoEndpoint = CloudEndpointUtils.
updateBuilder(new Taskinfoendpoint.Builder(
AndroidHttp.newCompatibleTransport(),
new JacksonFactory(),
new HttpRequestInitializer() {
public void initialize(HttpRequest
httpRequest) {
}
})).build();
return mTaskInfoEndpoint.listTaskInfo().execute().getItems();
} catch (IOException e) {
Log.e(TAG, “Error performing query.”, e);
cancel(true);
}
return null;
}
@Override
protected void onPostExecute(List<TaskInfo> taskInfos) {
super.onPostExecute(taskInfos);
mTaskInfoArrayAdapter.clear();
mTaskInfoArrayAdapter.addAll(taskInfos);
mTaskInfoArrayAdapter.notifyDataSetChanged();
}
}
The same approach can be used for updating and deleting existing TaskInfo objects.
Integrating a simple cloud backend can introduce some great features in your application. When the data your
application uses can be shared across all the user’s devices, including the user’s web browser, you can create
very powerful user experiences.
Google Cloud Messaging
When your application communicates with an online backend, such as a service implemented using Google
Cloud Endpoints, consider using server-side push notifications to notify the application when new data is
available. This reduces the need for devices to send unnecessary polling requests to your service, which drains
battery fast and also increases the load on your servers.
Google supports push notifications through the Google Cloud Messaging (GCM) service, which is part of
Google Play Services. Implementing GCM in your application allows you to send messages to the devices
running your application. The most common use for this is to let Android applications know that there’s new
data to fetch online; this is called a tickle and eliminates the need for doing recurring polling operations from
the application. You can also send application data with a payload of up to 4K using GCM.
To integrate GCM in your application, you must enable it in your Google API Console and set up a Server key for
your application under API Access. Also make sure that Google Cloud Messaging is enabled for your project.
GCM Client
On the client-side of your GCM integration, which is your Android application, you need to add a number of
elements to your manifest.
Here’s an example of how the manifest looks for the application with the package name gom.aptl.
myclouddemo.
<?xml version=”1.0” encoding=”utf-8”?>
<manifest xmlns:android=”http://schemas.android.com/apk/res/android”
package=”com.aptl.myclouddemo”
android:versionCode=”1”
android:versionName=”1.0”>
<uses-sdk
android:minSdkVersion=”18”
android:targetSdkVersion=”18”/>
<uses-permission
android:name=”com.google.android.c2dm.permission.RECEIVE”/>
<uses-permission android:name=”android.permission.INTERNET”/>
<uses-permission android:name=”android.permission.GET_ACCOUNTS”/>
<uses-permission android:name=”android.permission.WAKE_LOCK”/>
<permission
android:name=”com.aptl.myclouddemo.permission.C2D_MESSAGE”
android:protectionLevel=”signature”/>
<uses-permission
android:name=”com.aptl.myclouddemo.permission.C2D_
MESSAGE”/>
<application
android:allowBackup=”true”
android:icon=”@drawable/ic_launcher”
android:label=”@string/app_name”
android:theme=”@style/AppTheme”>
<activity
android:name=”.CloudPlatformDemo”
android:label=”@string/app_name”>
<intent-filter>
<action android:name=”android.intent.action.MAIN”/>
<category android:name=”android.intent.category.
LAUNCHER”/>
</intent-filter>
</activity>
<receiver
android:name=”.MyGcmReceiver”
android:permission=”com.google.android.c2dm.SEND”>
<intent-filter>
<action
android:name=”com.google.android.c2dm.intent.
RECEIVE”/>
<category android:name=”com.aptl.myclouddemo”/>
</intent-filter>
</receiver>
<service android:name=”.MyGcmService”/>
</application>
</manifest>
Note that the new permission declared prevents other applications from receiving the same incoming message.
Also, the permission on the receiver tag ensures that no other application can send a fake message to your
application.
The first time your application starts, you need to call GoogleCloudMessaging.register() using the
sender ID, as shown in the following example:
class RegisterGcm extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
try {
GoogleCloudMessaging cloudMessaging =
GoogleCloudMessaging.getInstance(CloudPlatformDemo.this);
String registrationID = cloudMessaging.register(SENDER_ID);
SharedPreferences preferences
= PreferenceManager
.getDefaultSharedPreferences(CloudPlatformDemo.this);
preferences.edit().
putString(PREFS_GCM_REG_ID, registrationID).
apply();
cloudMessaging.close();
} catch (IOException e) {
Log.e(TAG, “GCM Error.”, e);
}
return null;
}
}
The sender ID is the same as the project number that you can find on the Google Cloud Console. You will also
need to refresh the registration because it eventually times out. The default timeout for a GCM registration is
seven days.
Whenever your server sends a GCM message to your client, it triggers a call to the receiver registered for the
GCM messages. Following is a simple example where the message, stored as a Bundle in the Intent, is
passed to an IntentService for further processing:
public class MyGcmReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
GoogleCloudMessaging cloudMessaging =
GoogleCloudMessaging.getInstance(context);
String messageType = cloudMessaging.getMessageType(intent);
if(GoogleCloudMessaging.MESSAGE_TYPE_MESSAGE.equals(messageType))
{
// Pass the message to the IntentService for processing...
Intent serviceIntent = new Intent(context,
MyGcmService.class);
serviceIntent.putExtras(intent.getExtras());
context.startService(serviceIntent);
}
setResultCode(Activity.RESULT_OK);
}
}
Before your server can send GCM messages to a user’s device, the application must provide its registration ID
to the server. The easiest way to do so is to use the DeviceInfoEndpoint that’s generated by the Google
Cloud Endpoint features in Android Studio.
The AsyncTask shown next illustrates how to use the generated endpoint for delivering the registration ID to
the client:
class SendRegistrationId extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
try {
SharedPreferences preferences
= PreferenceManager
.getDefaultSharedPreferences(CloudPlatformDemo.this);
String registrationId = preferences.
getString(PREFS_GCM_REG_ID,
null);
DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.setDeviceRegistrationID(registrationId);
deviceInfo.setTimestamp(System.currentTimeMillis());
deviceInfo.setDeviceInformation(“Device Info...”);
Deviceinfoendpoint deviceinfoendpoint =
CloudEndpointUtils.updateBuilder(
new Deviceinfoendpoint.Builder(
AndroidHttp.newCompatibleTransport(),
new JacksonFactory(),
new HttpRequestInitializer() {
public void initialize(HttpRequest
httpRequest)
{
}
})
).build();
deviceinfoendpoint.insertDeviceInfo(deviceInfo).execute();
} catch (IOException e) {
Log.e(TAG, “Error inserting device info.”, e);
}
return null;
}
}
The final part is sending the GCM message from the server to the clients whenever a new TaskInfo has been
inserted (see previous section). The easiest way to do so is to modify the method TaskInfoEndpoint.
insertTaskInfo() in the App Engine module, as shown in the following code. The added code is shown in
bold. In this case, a simple tickle message is sent, telling the client that new data is available online.
@ApiMethod(name = “insertTaskInfo”)
public TaskInfo insertTaskInfo(TaskInfo taskInfo) throws IOException {
EntityManager mgr = getEntityManager();
try {
if (containsTaskInfo(taskInfo)) {
throw new EntityExistsException(“Object already exists”);
}
mgr.persist(taskInfo);
Sender sender = new Sender(API_KEY);
CollectionResponse<DeviceInfo> deviceInfos
= endpoint.listDeviceInfo(null, 10);
Message message = new Message.Builder().
addData(“message”, “Task Inserted”).build();
for (DeviceInfo deviceInfo : deviceInfos.getItems()) {
sender.send(message, deviceInfo.getDeviceRegistrationID(), 5);
}
} finally {
mgr.close();
}
return taskInfo;
}
Google Play Game Services
At Google IO 2013, developers were introduced to the new online services for games. Google Play Game
Services let developers include social elements such as achievements and leaderboards, but also real-time
multiplayer support in their games.
In this section, I cover the use of the real-time multiplayer support. This is the most technically advanced feature
in this service, and by mastering it, you should have no problem integrating the other features as well.
The core of the real-time multiplayer consists of two basic concepts: a virtual room where the game takes place
and a number of participants. Players can be invited to a room or automatically matched (auto-matching of
random players). A room is created when the first player wishes to start a new multiplayer session. All players
in a multiplayer game are participants in the same room. One player can send invitations to other players from
their Google+ circles.
The easiest way to implement the real-time multiplayer service in your game is to add the BaseGameUtils
library project to your application, which can be found at https://github.com/playgameservices/
android-samples. Here, you’ll find the class BaseGameActivity, which you should extend instead of the standard Activity class, and the GameHelper, which contains a number of useful helper methods.
Next, you need to enable the Google Play Games Services for your game. You can either follow the instructions
found at https://developers.google.com/games/services/console/enabling, or you can sign in to the Google API Console, enable the necessary services for your project (Google+ API, Google Play
Game Services, Google Play Game Management, and Google Play App State), and create a new Client ID for
installed applications, as shown in Figure 19-2. Next, take the first part of the Client ID (the numbers, followed
by .apps.googleusercontent.com) and add that as a string resource value in your application.
Now, you add a new metadata tag inside your applications tag of your manifest, as shown next. This connects
the Google Play Games Services to your application.
<meta-data android:name=”com.google.android.gms.games.APP_ID”
android:value=”@string/app_id” />
In the UI shown at startup, you add a button for the user to log in to Google+ with your game. If possible, use
the readymade SignInButton in your XML layout, as shown here:
<com.google.android.gms.common.SignInButton
android:id=”@+id/sign_in_button”
android:layout_centerHorizontal=”true”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:onClick=”doSignIn”/>
In the click-callback shown next, you simply call the method beginUserInitiatedSignIn() from the
base class BaseGameActivity, and the user can sign in:
public void doSignIn(View view) {
beginUserInitiatedSignIn();
}
You’ll have two callbacks for the sign-in process, onSignInFailed() and onSignInSucceeded(), which
are used to process the result. If sign-in is successful, you can proceed by displaying a UI for inviting players or
seeing existing invitations.
In the two click-callbacks shown in the following code, you invite new players or display existing invitations by
starting the games client Activity:
public void doInvitePlayers(View view) {
Intent intent = getGamesClient().getSelectPlayersIntent(1, 3);
startActivityForResult(intent, INVITE_PLAYERS_RC, null);
}
public void doSeeInvitations(View view) {
Intent intent = getGamesClient().getInvitationInboxIntent();
startActivityForResult(intent, SEE_INVITATIONS_RC, null);
}
When inviting players, you must specify the maximum and minimum number of players for a game. These calls
open a new Activity from the Google Play Game Services framework that lets the user either pick opponents
or invite additional players.
Because there are two ways to start a multiplayer game, either through an invitation or by inviting players, you
need two different methods for creating the room for this game session. The following code shows how to
handle these two cases:
private void handlePlayersInvited(Intent intent) {
ArrayList<String> players = intent.
getStringArrayListExtra(GamesClient.EXTRA_PLAYERS);
int minAutoMatchPlayers = intent.
getIntExtra(GamesClient.EXTRA_MIN_AUTOMATCH_PLAYERS, 0);
int maxAutoMatchPlayers = intent.
getIntExtra(GamesClient.EXTRA_MAX_AUTOMATCH_PLAYERS, 0);
Bundle autoMatchCriteria = null;
if (minAutoMatchPlayers > 0 || maxAutoMatchPlayers > 0) {
autoMatchCriteria =
RoomConfig.createAutoMatchCriteria(minAutoMatchPlayers,
maxAutoMatchPlayers, 0);
}
RoomConfig.Builder roomConfigBuilder
= RoomConfig.builder(this);
roomConfigBuilder.addPlayersToInvite(players);
roomConfigBuilder.setMessageReceivedListener(this);
roomConfigBuilder.setRoomStatusUpdateListener(this);
if (autoMatchCriteria != null) {
roomConfigBuilder.setAutoMatchCriteria(autoMatchCriteria);
}
getGamesClient().createRoom(roomConfigBuilder.build());
}
private void handleInvitationResult(Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle != null) {
Invitation invitation =
bundle.getParcelable(GamesClient.EXTRA_INVITATION);
if(invitation != null) {
RoomConfig.Builder roomConfigBuilder
= RoomConfig.builder(this);
roomConfigBuilder
.setInvitationIdToAccept(invitation.getInvitationId())
.setMessageReceivedListener(this)
.setRoomStatusUpdateListener(this);
getGamesClient().joinRoom(roomConfigBuilder.build());
}
}
}
The two interfaces RoomStatusUpdateListener and RoomUpdateListener provide a set of callback
methods for the different state changes of your multiplayer session. How you use these callbacks depends
completely on your game. The simplest solution is to direct all callbacks to the same handler where you
determine the state of the game, as shown in the following code:
@Override
public void onPeerJoined(Room room, List<String> arg1) {
updateRoom(room);
}
public void updateGameState(Room room) {
mParticipants = room.getParticipants();
// TODO: Implement change of game state here...
}
The first method shown here is one of the callbacks from RoomStatusUpdateListener. In this case, you
simply call updateGameState() with the Room as a parameter. In this method, you get the current list of
participants and then update the game state accordingly. Remember that a participant can disconnect from an
ongoing game at any time, so your code needs to deal with this and act accordingly.
During the game, each player will perform actions that will result in an update being sent to the other players.
The following code shows how to send a real-time message to all participants in a room.
public void sendGameUpdate(byte[] data, Room room) {
getGamesClient().sendUnreliableRealTimeMessageToAll(data,
room.getRoomId());
}
Data Messaging
The messaging part of the real-time multiplayer API is very interesting. It allows you to send messages with data
between players in a Room without any additional servers or other infrastructure needed by you. Three types of
communication are supported by this API:
■ Reliable messaging: Allows you to send a message with a maximum of 1400 bytes with reliable data
delivery, integrity, and ordering. This means that all messages will arrive and in the right order. However,
because of network latency and such, there can be a significant delay between two messages. This should
be used for data that needs to be delivered but where latency is not an issue.
■ Unreliable messaging: This allows you to send messages with a maximum of 1168 bytes. There is no
guarantee of delivery or data arriving in order. Integrity of individual messages is preserved, but you need
to implement your game logic so that it doesn’t depend on every one of these messages being delivered or
if they’re delivered in the right order. These messages usually have a much lower latency than the reliable
messages and can thus be used for a real-time scenario.
■ Socket-based messaging: This enables streaming of data between players, instead of delivering distinct
messages. The socket-based messaging has the same limits as unreliable messaging, but usually has lower
latency than reliable messaging. However, this method of communication is more complex, and you should
use unreliable or reliable messaging as much as possible.
All three types of messaging are done through the GameClient class. Receiving messages is handled by
adding a RealTimeMessageReceivedListener to the Room before starting a game session.
Messaging Strategy
When using the real-time multiplayer API, your players can connect across networks. Google’s infrastructure
behind these services allows players to be on completely different networks and still be able to send messages
with relatively low latency.
However, you still need a smart strategy for sending messages between participants in a game. Although
the name of the service implies real-time aspects, that’s not altogether the case. There may be hundreds
of milliseconds or even seconds in latency between two players, depending on their respective network
configuration.
The goal is to send as few messages as possible and only with data that’s absolutely necessary. For instance, in
the case of a multiplayer racing game, you need to communicate two things from a participant: current velocity
(speed and direction) and current position. Each instance of the game then calculates the position of the
local player’s cars and updates the view accordingly. The position of the different players won’t match exactly
between the players, but it will be close enough for the game to feel consistent.
When a player changes the speed or direction of his car, your game will send an update with this information to
the other participants. Because this will change frequently, these messages should be sent as unreliable messages
to reduce latency. When receiving one of these messages, the game will update the speed and direction of the
local representation of that participant accordingly. Because the order for these messages isn’t guaranteed, they
should also contain a sequence number so that your game can discard messages that arrive out of order.
Because unreliable messages might also be lost, you also need to send a few reliable messages that can be used
as more reliable checkpoints in the game. This way, even if one participant drops a number of messages with
speed and direction updates, the other players will eventually receive the correct position for that player.
The following example uses the class Car to represent each participant in the racing game:
class Car {
String participantId;
Point position;
Point velocity;
}
public static final byte POSITION_DATA = 1;
public static final byte VELOCITY_DATA = 2;
private HashMap<String, Car> mCars = new HashMap<String, Car>();
@Override
public void onRealTimeMessageReceived(RealTimeMessage realTimeMessage) {
String participantId = realTimeMessage.getSenderParticipantId();
Car car = mCars.get(participantId);
byte[] messageData = realTimeMessage.getMessageData();
switch (messageData[0]) {
case POSITION_DATA:
car.position.set(messageData[1], messageData[2]);
break;
case VELOCITY_DATA:
car.velocity.set(messageData[1], messageData[2]);
break;
}
}
public void simpleRaceGameLoop() {
for (Car car : mCars.values()) {
car.position.offset(car.velocity.x, car.velocity.y);
}
SystemClock.sleep(100);
}
Whenever a message arrives, you check the first byte to see if it’s an update to a car’s position or its velocity.
Here, you update the state of the variable for that Car.
Meanwhile, the simpleRaceGameLoop()loops continuously during the game and updates the position
of each car using the last known velocity. This method is called dead-reckoning and is used in many types of
multiplayer games.
For a real game, you need to add additional information to these messages and also implement some logic to
handle the cases when the player crashes the car. Also, you need to have a separate message to determine who
the winner is, and in this case, the easiest way is to let each participant game calculate his finishing time and
broadcast it. This way, each participant constructs the final result using the input from the other participants.
The preceding example uses the raw byte array directly for the necessary data, which can work fine if you have
only a small variation of your messages and don’t send complex data structures. However, as soon as your
messages increase in complexity, I highly recommend that you use Google Protocol Buffers. See Chapter 9 for
details on how to integrate this into your application.
Implementing real-time multiplayer games is usually very complicated. But with the new Google Play Game
Services APIs, implementation has now became much easier. You no longer need to deal with all the difficult
aspects of networking and can focus on the actual game content, thus I expect a lot of really innovative
multiplayer games to appear.
Summary
By adding a cloud backend to your application, you can greatly enhance the user experience. Many users have
more than one device, and by utilizing the online services available for syncing data or interacting with existing
Google services, your application can fit more seamlessly into the entire ecosystem.
The Google Play Services APIs for Android provide a set of very powerful APIs for authorization, real-time
multiplayer integration, and push notifications using GCM, to name a few. You can select the parts of these
services and APIs that are most appropriate for your application.
Because most of these services have a free tier on the Google Cloud Platform, you can start experimenting to
get a better understanding of how they work and what they can provide.
Further Resources Websites
Main site for Google Play Services: http://developer.android.com/google/index.html
Main site for Google Cloud Platform: https://cloud.google.com
Google Play Game Services samples and helper libraries: https://github.com/playgameservices/android-samples
Multiplayer API for Google Play Games Services: https://developers.google.com/games/services/common/concepts/realtimeMultiplayer
Synchronized stopwatch demo using Google Cloud Endpoints and Cloud Messaging: http://
bradabrams.com/2013/06/google-io-2013-demo-android-studio-cloud-endpoints-synchronized-stopwatch-demo