In 11.6.0 of the Google Play Services SDK, we are introducing a major change to how the APIs are structured. We've deprecated the GoogleApiClient class, and introduced a decoupled collection of feature-specific API clients. This reduces the amount of boilerplate code required to access the Play Games Services APIs.
The change in the APIs is meant to make the APIs easier to use, thread-safe, and
more memory efficient. The new API model also makes use of the Task
model to give better separation of the concerns between your activity and
handling the asynchronous results of the APIs. This programming model first
appeared in Firebase and was well received. To dive in deeper into the
wonderful world of Tasks, check out the blog
series on tasks and the Tasks API developer
guide.
As always, the developer documentation is a reliable source of information on these APIs, as well as all the other Google resources for developers. The Android Basic Samples project, and Client Server Skeleton project have both been updated to use the Play Services API clients so you can see them in action. These sample projects are also the best place to add issues or problems you encounter using these APIs.
These changes seem big, but fear not! Using the Play Services API clients is very simple and will result in much less clutter in your code. There are three parts to using the API clients:
The details of the authentication process are found on the Google Developers website.
The most common use case for authentication is to use the DEFAULT_GAMES_SIGN_IN option. This option enables your game to use the games profile for the user. Since a user's games profile only contains a gamer tag that your game can display like a name, and an avatar for a image, the actual identity of the user is protected. This eliminates the need for the user to consent to sharing any additional personal information reducing the friction between the user and your game.
Note: The only Google sign-in option that can be requested which only uses games profile is requestServerAuthCode(). All others, such as requestIDToken() require the user to consent to additional information being shared. This also has the effect of preventing users from having a zero-tap sign-in experience.
One more note: if you are using the Snapshots API to save game data, you need to add the Drive.SCOPE_APPFOLDER scope when building the sign-in options:
private GoogleSignInClient signInClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // other code here GoogleSignInOptions signInOption = new GoogleSignInOptions.Builder( GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) // If you are using Snapshots add the Drive scope. .requestScopes(Drive.SCOPE_APPFOLDER) // If you need a server side auth code, request it here. .requestServerAuthCode(webClientId) .build(); signInClient = GoogleSignIn.getClient(context, signInOption); }
Since there can only be one user account signed in at a time, it's good practice to attempt a silent sign-in when the activity is resuming. This will have the effect of automatically signing in the user if it is valid to do so. It will also update or invalidate the signed-in account if there have been any changes, such as the user signing out from another activity.
private void signInSilently() { GoogleSignInOptions signInOption = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN) .build(); GoogleSignInClient signInClient = GoogleSignIn.getClient(this, signInOption); signInClient.silentSignIn().addOnCompleteListener(this, new OnCompleteListener<GoogleSignInAccount>() { @Override public void onComplete(@NonNull Task<GoogleSignInAccount> task) { // Handle UI updates based on being signed in or not. enableUIButtons(task.isSuccessful()); // It is OK to cache the account for later use. mSignInAccount = task.getResult(); } }); }
@Override protected void onResume() { super.onResume(); signInSilently(); }
Signing in interactively is done by launching a separate intent. This is great! No more checking to see if errors have resolution and then trying to call the right APIs to resolve them. Just simply start the activity, and get the result in onActivityResult().
Intent intent = signInClient.getSignInIntent(); startActivityForResult(intent, RC_SIGN_IN);
@Override protected void onActivityResult(int requestCode, int resultCode, Intent intent) { super.onActivityResult(requestCode, resultCode, intent); if (requestCode == RC_SIGN_IN) { // The Task returned from this call is always completed, no need to attach // a listener. Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(intent); try { GoogleSignInAccount account = task.getResult(ApiException.class); // Signed in successfully, show authenticated UI. enableUIButtons(true); } catch (ApiException apiException) { // The ApiException status code indicates the // detailed failure reason. // Please refer to the GoogleSignInStatusCodes class reference // for more information. Log.w(TAG, "signInResult:failed code= " + apiException.getStatusCode()); new AlertDialog.Builder(MainActivity.this) .setMessage("Signin Failed") .setNeutralButton(android.R.string.ok, null) .show(); } } }
To determine if a user is signed in, you can call the GoogleSignIn.getLastSignedInAccount() method. This returns the GoogleSignInAccount for the user that is signed in, or null if no user is signed in.
if (GoogleSignIn.getLastSignedInAccount(/*context*/ this) != null) { // There is a user signed in, handle updating the UI. enableUIButtons(true); } else { // Not signed in; update the UI. enableUIButtons(false); }
Signing out is done by calling GoogleSignInClient.signOut(). There is no longer a Games specific sign-out.
signInClient.signOut().addOnCompleteListener(MainActivity.this, new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { enableUIButtons(false); } }); );
In previous versions of Play Games Services, the general pattern of calling an API was something like this:
PendingResult<Stats.LoadPlayerStatsResult> result = Games.Stats.loadPlayerStats( mGoogleApiClient, false /* forceReload */); result.setResultCallback(new ResultCallback<Stats.LoadPlayerStatsResult>() { public void onResult(Stats.LoadPlayerStatsResult result) { Status status = result.getStatus(); if (status.isSuccess()) { PlayerStats stats = result.getPlayerStats(); if (stats != null) { Log.d(TAG, "Player stats loaded"); if (stats.getDaysSinceLastPlayed() > 7) { Log.d(TAG, "It's been longer than a week"); } if (stats.getNumberOfSessions() > 1000) { Log.d(TAG, "Veteran player"); } if (stats.getChurnProbability() == 1) { Log.d(TAG, "Player is at high risk of churn"); } } } else { Log.d(TAG, "Failed to fetch Stats Data status: " + status.getStatusMessage()); } } });
The API was accessed from a static field on the Games class, the API returned a PendingResult, which you added a listener to in order to get the result.
Now things have changed slightly. There is a static method to get the API client from the Games class, and the Task class has replaced the PendingResult class.
As a result, the new code looks like this:
GoogleSignInAccount mSignInAccount = null; Games.getPlayerStatsClient(this, mSignInAccount).loadPlayerStats(true) .addOnCompleteListener( new OnCompleteListener<AnnotatedData<PlayerStats>>() { @Override public void onComplete(Task<AnnotatedData<PlayerStats>> task) { try { AnnotatedData<PlayerStats> statsData = task.getResult(ApiException.class); if (statsData.isStale()) { Log.d(TAG,"using cached data"); } PlayerStats stats = statsData.get(); if (stats != null) { Log.d(TAG, "Player stats loaded"); if (stats.getDaysSinceLastPlayed() > 7) { Log.d(TAG, "It's been longer than a week"); } if (stats.getNumberOfSessions() > 1000) { Log.d(TAG, "Veteran player"); } if (stats.getChurnProbability() == 1) { Log.d(TAG, "Player is at high risk of churn"); } } } catch (ApiException apiException) { int status = apiException.getStatusCode(); Log.d(TAG, "Failed to fetch Stats Data status: " + status + ": " + task.getException()); } } });
So, as you can see, the change is not too big, but you will gain all the goodness of the Task API, and not have to worry about the GoogleApiClient lifecycle management.
The pattern of changes is the same for all the APIs. If you need more information, you can consult the Developer website. For example if you used Games.Achievements, you now need to use Games.getAchievementClient().
The last major change to the Play Games Services APIs is the introduction of a new API class, GamesClient. This class handles support methods such as setGravityForPopups(), getSettingsIntent(), and also provides access to the multiplayer invitation object when your game is launched from a notification.
Previously the onConnected() method was called with a connection hint. This hint was a Bundle object that could contain the invitation that was passed to the activity when starting.
Now using the GamesClient API, if there is an invitation, your game should call signInSilently(); this call will succeed since the user is known from the invitation. Then retrieve the activation hint and process the invitation if present by calling GamesClient.getActivationHint():
Games.getGamesClient(MainActivity.this, mSignInAccount) .getActivationHint().addOnCompleteListener( new OnCompleteListener<Bundle>() { @Override public void onComplete(@NonNull Task<Bundle> task) { try { Bundle hint = task.getResult(ApiException.class); if (hint != null) { Invitation inv = hint.getParcelable(Multiplayer.EXTRA_INVITATION); if (inv != null && inv.getInvitationId() != null) { // retrieve and cache the invitation ID acceptInviteToRoom(inv.getInvitationId()); return; } } } catch (ApiException apiException) { Log.w(TAG, "getActivationHint failed: " + apiException.getMessage()); } } });
When a method call fails, the Task.isSuccessful()
will be false and information about the failure is accessed by calling Task.getException()
.
In some cases the exception is simply a non-success return value from the API
call. You can check for this by casting to an ApiException
:
if (task.getException() instanceof ApiException) { ApiException apiException = (ApiException) task.getException(); status = apiException.getStatusCode(); }
In other cases, a MatchApiException can be returned and contains updated match data structure. It can be retrieved in a similar manner:
if (task.getException() instanceof MatchApiException) { MatchApiException matchApiException = (MatchApiException) task.getException(); status = matchApiException.getStatusCode(); match = matchApiException.getMatch(); } else if (task.getException() instanceof ApiException) { ApiException apiException = (ApiException) task.getException(); status = apiException.getStatusCode(); }
If the status code is SIGN_IN_REQUIRED, this indicates that the player needs to be re-authenticated. To do this, call GoogleSignInClient.getSignInIntent() to sign in the player interactively.
The change from the GoogleApiClient usage to a more loosely coupled API clients usage will provide benefits of less boilerplate code, more clear usage patterns, and thread safety. As you migrate your current game to API clients, refer to these resources:
Sign-In Best practices for Games:
https://developers.google.com/games/services/checklist
Play Games Services Samples:
StackOverflow:
https://stackoverflow.com/questions/tagged/google-play-games