Migrating to the new Play Games Services APIs

November 09, 2017


Link copied to clipboard

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:

  1. Authentication is now explicitly done using the Google Sign-In client. This makes it more clear how to control the authentication process and the difference between a Google Sign-In identity and the Play Games Services identity.
  2. Convert all the Games.category static method calls to use the corresponding API client methods. This also includes converting PendingResult usages to use the Task class. The Task model helps greatly with separation of concerns in your code, and reduces the amount of multi-threaded complexity since tasks listeners are called back on the UI thread.
  3. Handling multi-player invitations is done explicitly through the turn-based and real-time multiplayer API clients. Since GoogleApiClient is no longer used, there is no access to the "connection hint" object which contains multi-player invitations. The invitation is now accessed through an explicit method call.

Authentication

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

Using Games Services API clients

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

Handling failure

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.

Summary

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:

Android Basic Samples

Client Server Skeleton

StackOverflow:

https://stackoverflow.com/questions/tagged/google-play-games