| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149 |
- /*
- * Copyright (C) 2013 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package org.gameplay3d.sample_spaceship.basegameutils;
- import java.security.MessageDigest;
- import java.security.NoSuchAlgorithmException;
- import java.util.Vector;
- import android.app.Activity;
- import android.app.AlertDialog;
- import android.app.Dialog;
- import android.content.Context;
- import android.content.Intent;
- import android.content.IntentSender.SendIntentException;
- import android.content.pm.PackageManager;
- import android.content.pm.Signature;
- import android.content.res.Resources;
- import android.os.Bundle;
- import android.util.Log;
- import android.view.Gravity;
- import com.google.android.gms.appstate.AppStateClient;
- import com.google.android.gms.common.ConnectionResult;
- import com.google.android.gms.common.GooglePlayServicesClient;
- import com.google.android.gms.common.GooglePlayServicesUtil;
- import com.google.android.gms.common.Scopes;
- import com.google.android.gms.games.GamesActivityResultCodes;
- import com.google.android.gms.games.GamesClient;
- import com.google.android.gms.games.multiplayer.Invitation;
- import com.google.android.gms.plus.PlusClient;
- public class GameHelper implements GooglePlayServicesClient.ConnectionCallbacks,
- GooglePlayServicesClient.OnConnectionFailedListener {
- /** Listener for sign-in success or failure events. */
- public interface GameHelperListener {
- /**
- * Called when sign-in fails. As a result, a "Sign-In" button can be
- * shown to the user; when that button is clicked, call
- * @link{GamesHelper#beginUserInitiatedSignIn}. Note that not all calls to this
- * method mean an error; it may be a result of the fact that automatic
- * sign-in could not proceed because user interaction was required
- * (consent dialogs). So implementations of this method should NOT
- * display an error message unless a call to @link{GamesHelper#hasSignInError}
- * indicates that an error indeed occurred.
- */
- void onSignInFailed();
- /** Called when sign-in succeeds. */
- void onSignInSucceeded();
- }
- // States we can be in
- public static final int STATE_UNCONFIGURED = 0;
- public static final int STATE_DISCONNECTED = 1;
- public static final int STATE_CONNECTING = 2;
- public static final int STATE_CONNECTED = 3;
- // State names (for debug logging, etc)
- public static final String[] STATE_NAMES = {
- "UNCONFIGURED", "DISCONNECTED", "CONNECTING", "CONNECTED"
- };
- // State we are in right now
- int mState = STATE_UNCONFIGURED;
- // Are we expecting the result of a resolution flow?
- boolean mExpectingResolution = false;
- /**
- * The Activity we are bound to. We need to keep a reference to the Activity
- * because some games methods require an Activity (a Context won't do). We
- * are careful not to leak these references: we release them on onStop().
- */
- Activity mActivity = null;
- // OAuth scopes required for the clients. Initialized in setup().
- String mScopes[];
- // Request code we use when invoking other Activities to complete the
- // sign-in flow.
- final static int RC_RESOLVE = 9001;
- // Request code when invoking Activities whose result we don't care about.
- final static int RC_UNUSED = 9002;
- // Client objects we manage. If a given client is not enabled, it is null.
- GamesClient mGamesClient = null;
- PlusClient mPlusClient = null;
- AppStateClient mAppStateClient = null;
- // What clients we manage (OR-able values, can be combined as flags)
- public final static int CLIENT_NONE = 0x00;
- public final static int CLIENT_GAMES = 0x01;
- public final static int CLIENT_PLUS = 0x02;
- public final static int CLIENT_APPSTATE = 0x04;
- public final static int CLIENT_ALL = CLIENT_GAMES | CLIENT_PLUS | CLIENT_APPSTATE;
- // What clients were requested? (bit flags)
- int mRequestedClients = CLIENT_NONE;
- // What clients are currently connected? (bit flags)
- int mConnectedClients = CLIENT_NONE;
- // What client are we currently connecting?
- int mClientCurrentlyConnecting = CLIENT_NONE;
- // Whether to automatically try to sign in on onStart().
- boolean mAutoSignIn = true;
- /*
- * Whether user has specifically requested that the sign-in process begin.
- * If mUserInitiatedSignIn is false, we're in the automatic sign-in attempt
- * that we try once the Activity is started -- if true, then the user has
- * already clicked a "Sign-In" button or something similar
- */
- boolean mUserInitiatedSignIn = false;
- // The connection result we got from our last attempt to sign-in.
- ConnectionResult mConnectionResult = null;
- // The error that happened during sign-in.
- SignInFailureReason mSignInFailureReason = null;
- // Print debug logs?
- boolean mDebugLog = false;
- String mDebugTag = "GameHelper";
- /*
- * If we got an invitation id when we connected to the games client, it's
- * here. Otherwise, it's null.
- */
- String mInvitationId;
- // Listener
- GameHelperListener mListener = null;
- /**
- * Construct a GameHelper object, initially tied to the given Activity.
- * After constructing this object, call @link{setup} from the onCreate()
- * method of your Activity.
- */
- public GameHelper(Activity activity) {
- mActivity = activity;
- }
- static private final int TYPE_DEVELOPER_ERROR = 1001;
- static private final int TYPE_GAMEHELPER_BUG = 1002;
- boolean checkState(int type, String operation, String warning, int... expectedStates) {
- for (int expectedState : expectedStates) {
- if (mState == expectedState) {
- return true;
- }
- }
- StringBuilder sb = new StringBuilder();
- if (type == TYPE_DEVELOPER_ERROR) {
- sb.append("GameHelper: you attempted an operation at an invalid. ");
- } else {
- sb.append("GameHelper: bug detected. Please report it at our bug tracker ");
- sb.append("https://github.com/playgameservices/android-samples/issues. ");
- sb.append("Please include the last couple hundred lines of logcat output ");
- sb.append("and describe the operation that caused this. ");
- }
- sb.append("Explanation: ").append(warning);
- sb.append("Operation: ").append(operation).append(". ");
- sb.append("State: ").append(STATE_NAMES[mState]).append(". ");
- if (expectedStates.length == 1) {
- sb.append("Expected state: ").append(STATE_NAMES[expectedStates[0]]).append(".");
- } else {
- sb.append("Expected states:");
- for (int expectedState : expectedStates) {
- sb.append(" " ).append(STATE_NAMES[expectedState]);
- }
- sb.append(".");
- }
- logWarn(sb.toString());
- return false;
- }
- void assertConfigured(String operation) {
- if (mState == STATE_UNCONFIGURED) {
- String error = "GameHelper error: Operation attempted without setup: " + operation +
- ". The setup() method must be called before attempting any other operation.";
- logError(error);
- throw new IllegalStateException(error);
- }
- }
- /**
- * Same as calling @link{setup(GameHelperListener, int)} requesting only the
- * CLIENT_GAMES client.
- */
- public void setup(GameHelperListener listener) {
- setup(listener, CLIENT_GAMES);
- }
- /**
- * Performs setup on this GameHelper object. Call this from the onCreate()
- * method of your Activity. This will create the clients and do a few other
- * initialization tasks. Next, call @link{#onStart} from the onStart()
- * method of your Activity.
- *
- * @param listener The listener to be notified of sign-in events.
- * @param clientsToUse The clients to use. Use a combination of
- * CLIENT_GAMES, CLIENT_PLUS and CLIENT_APPSTATE, or CLIENT_ALL
- * to request all clients.
- * @param additionalScopes Any scopes to be used that are outside of the ones defined
- * in the Scopes class.
- * I.E. for YouTube uploads one would add
- * "https://www.googleapis.com/auth/youtube.upload"
- */
- public void setup(GameHelperListener listener, int clientsToUse, String ... additionalScopes) {
- if (mState != STATE_UNCONFIGURED) {
- String error = "GameHelper: you called GameHelper.setup() twice. You can only call " +
- "it once.";
- logError(error);
- throw new IllegalStateException(error);
- }
- mListener = listener;
- mRequestedClients = clientsToUse;
- debugLog("Setup: requested clients: " + mRequestedClients);
- Vector<String> scopesVector = new Vector<String>();
- if (0 != (clientsToUse & CLIENT_GAMES)) {
- scopesVector.add(Scopes.GAMES);
- }
- if (0 != (clientsToUse & CLIENT_PLUS)) {
- scopesVector.add(Scopes.PLUS_LOGIN);
- }
- if (0 != (clientsToUse & CLIENT_APPSTATE)) {
- scopesVector.add(Scopes.APP_STATE);
- }
- if (null != additionalScopes) {
- for (String scope : additionalScopes) {
- scopesVector.add(scope);
- }
- }
- mScopes = new String[scopesVector.size()];
- scopesVector.copyInto(mScopes);
- debugLog("setup: scopes:");
- for (String scope : mScopes) {
- debugLog(" - " + scope);
- }
- if (0 != (clientsToUse & CLIENT_GAMES)) {
- debugLog("setup: creating GamesClient");
- mGamesClient = new GamesClient.Builder(getContext(), this, this)
- .setGravityForPopups(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
- .setScopes(mScopes)
- .create();
- }
- if (0 != (clientsToUse & CLIENT_PLUS)) {
- debugLog("setup: creating GamesPlusClient");
- mPlusClient = new PlusClient.Builder(getContext(), this, this)
- .setScopes(mScopes)
- .build();
- }
- if (0 != (clientsToUse & CLIENT_APPSTATE)) {
- debugLog("setup: creating AppStateClient");
- mAppStateClient = new AppStateClient.Builder(getContext(), this, this)
- .setScopes(mScopes)
- .create();
- }
- setState(STATE_DISCONNECTED);
- }
- void setState(int newState) {
- String oldStateName = STATE_NAMES[mState];
- String newStateName = STATE_NAMES[newState];
- mState = newState;
- debugLog("State change " + oldStateName + " -> " + newStateName);
- }
- /**
- * Returns the GamesClient object. In order to call this method, you must have
- * called @link{setup} with a set of clients that includes CLIENT_GAMES.
- */
- public GamesClient getGamesClient() {
- if (mGamesClient == null) {
- throw new IllegalStateException("No GamesClient. Did you request it at setup?");
- }
- return mGamesClient;
- }
- /**
- * Returns the AppStateClient object. In order to call this method, you must have
- * called @link{#setup} with a set of clients that includes CLIENT_APPSTATE.
- */
- public AppStateClient getAppStateClient() {
- if (mAppStateClient == null) {
- throw new IllegalStateException("No AppStateClient. Did you request it at setup?");
- }
- return mAppStateClient;
- }
- /**
- * Returns the PlusClient object. In order to call this method, you must have
- * called @link{#setup} with a set of clients that includes CLIENT_PLUS.
- */
- public PlusClient getPlusClient() {
- if (mPlusClient == null) {
- throw new IllegalStateException("No PlusClient. Did you request it at setup?");
- }
- return mPlusClient;
- }
- /** Returns whether or not the user is signed in. */
- public boolean isSignedIn() {
- return mState == STATE_CONNECTED;
- }
- /**
- * Returns whether or not there was a (non-recoverable) error during the
- * sign-in process.
- */
- public boolean hasSignInError() {
- return mSignInFailureReason != null;
- }
- /**
- * Returns the error that happened during the sign-in process, null if no
- * error occurred.
- */
- public SignInFailureReason getSignInError() {
- return mSignInFailureReason;
- }
- /** Call this method from your Activity's onStart(). */
- public void onStart(Activity act) {
- mActivity = act;
- debugLog("onStart, state = " + STATE_NAMES[mState]);
- assertConfigured("onStart");
- switch (mState) {
- case STATE_DISCONNECTED:
- // we are not connected, so attempt to connect
- if (mAutoSignIn) {
- debugLog("onStart: Now connecting clients.");
- startConnections();
- } else {
- debugLog("onStart: Not connecting (user specifically signed out).");
- }
- break;
- case STATE_CONNECTING:
- // connection process is in progress; no action required
- debugLog("onStart: connection process in progress, no action taken.");
- break;
- case STATE_CONNECTED:
- // already connected (for some strange reason). No complaints :-)
- debugLog("onStart: already connected (unusual, but ok).");
- break;
- default:
- String msg = "onStart: BUG: unexpected state " + STATE_NAMES[mState];
- logError(msg);
- throw new IllegalStateException(msg);
- }
- }
- /** Call this method from your Activity's onStop(). */
- public void onStop() {
- debugLog("onStop, state = " + STATE_NAMES[mState]);
- assertConfigured("onStop");
- switch (mState) {
- case STATE_CONNECTED:
- case STATE_CONNECTING:
- // kill connections
- debugLog("onStop: Killing connections");
- killConnections();
- break;
- case STATE_DISCONNECTED:
- debugLog("onStop: not connected, so no action taken.");
- break;
- default:
- String msg = "onStop: BUG: unexpected state " + STATE_NAMES[mState];
- logError(msg);
- throw new IllegalStateException(msg);
- }
- // let go of the Activity reference
- mActivity = null;
- }
- /** Convenience method to show an alert dialog. */
- public void showAlert(String title, String message) {
- (new AlertDialog.Builder(getContext())).setTitle(title).setMessage(message)
- .setNeutralButton(android.R.string.ok, null).create().show();
- }
- /** Convenience method to show an alert dialog. */
- public void showAlert(String message) {
- (new AlertDialog.Builder(getContext())).setMessage(message)
- .setNeutralButton(android.R.string.ok, null).create().show();
- }
- /**
- * Returns the invitation ID received through an invitation notification.
- * This should be called from your GameHelperListener's
- *
- * @link{GameHelperListener#onSignInSucceeded} method, to check if there's an
- * invitation available. In that case, accept the invitation.
- * @return The id of the invitation, or null if none was received.
- */
- public String getInvitationId() {
- if (!checkState(TYPE_DEVELOPER_ERROR, "getInvitationId",
- "Invitation ID is only available when connected " +
- "(after getting the onSignInSucceeded callback).", STATE_CONNECTED)) {
- return null;
- }
- return mInvitationId;
- }
- /** Enables debug logging */
- public void enableDebugLog(boolean enabled, String tag) {
- mDebugLog = enabled;
- mDebugTag = tag;
- if (enabled) {
- debugLog("Debug log enabled, tag: " + tag);
- }
- }
- /**
- * Returns the current requested scopes. This is not valid until setup() has
- * been called.
- *
- * @return the requested scopes, including the oauth2: prefix
- */
- public String getScopes() {
- StringBuilder scopeStringBuilder = new StringBuilder();
- if (null != mScopes) {
- for (String scope: mScopes) {
- addToScope(scopeStringBuilder, scope);
- }
- }
- return scopeStringBuilder.toString();
- }
- /**
- * Returns an array of the current requested scopes. This is not valid until
- * setup() has been called
- *
- * @return the requested scopes, including the oauth2: prefix
- */
- public String[] getScopesArray() {
- return mScopes;
- }
- /** Sign out and disconnect from the APIs. */
- public void signOut() {
- if (mState == STATE_DISCONNECTED) {
- // nothing to do
- debugLog("signOut: state was already DISCONNECTED, ignoring.");
- return;
- }
- // for the PlusClient, "signing out" means clearing the default account and
- // then disconnecting
- if (mPlusClient != null && mPlusClient.isConnected()) {
- debugLog("Clearing default account on PlusClient.");
- mPlusClient.clearDefaultAccount();
- }
- // For the games client, signing out means calling signOut and disconnecting
- if (mGamesClient != null && mGamesClient.isConnected()) {
- debugLog("Signing out from GamesClient.");
- mGamesClient.signOut();
- }
- // Ready to disconnect
- debugLog("Proceeding with disconnection.");
- killConnections();
- }
- void killConnections() {
- if (!checkState(TYPE_GAMEHELPER_BUG, "killConnections", "killConnections() should only " +
- "get called while connected or connecting.", STATE_CONNECTED, STATE_CONNECTING)) {
- return;
- }
- debugLog("killConnections: killing connections.");
- mConnectionResult = null;
- mSignInFailureReason = null;
- if (mGamesClient != null && mGamesClient.isConnected()) {
- debugLog("Disconnecting GamesClient.");
- mGamesClient.disconnect();
- }
- if (mPlusClient != null && mPlusClient.isConnected()) {
- debugLog("Disconnecting PlusClient.");
- mPlusClient.disconnect();
- }
- if (mAppStateClient != null && mAppStateClient.isConnected()) {
- debugLog("Disconnecting AppStateClient.");
- mAppStateClient.disconnect();
- }
- mConnectedClients = CLIENT_NONE;
- debugLog("killConnections: all clients disconnected.");
- setState(STATE_DISCONNECTED);
- }
- static String activityResponseCodeToString(int respCode) {
- switch (respCode) {
- case Activity.RESULT_OK:
- return "RESULT_OK";
- case Activity.RESULT_CANCELED:
- return "RESULT_CANCELED";
- case GamesActivityResultCodes.RESULT_APP_MISCONFIGURED:
- return "RESULT_APP_MISCONFIGURED";
- case GamesActivityResultCodes.RESULT_LEFT_ROOM:
- return "RESULT_LEFT_ROOM";
- case GamesActivityResultCodes.RESULT_LICENSE_FAILED:
- return "RESULT_LICENSE_FAILED";
- case GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED:
- return "RESULT_RECONNECT_REQUIRED";
- case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED:
- return "SIGN_IN_FAILED";
- default:
- return String.valueOf(respCode);
- }
- }
- /**
- * Handle activity result. Call this method from your Activity's
- * onActivityResult callback. If the activity result pertains to the sign-in
- * process, processes it appropriately.
- */
- public void onActivityResult(int requestCode, int responseCode, Intent intent) {
- debugLog("onActivityResult: req=" + (requestCode == RC_RESOLVE ? "RC_RESOLVE" :
- String.valueOf(requestCode)) + ", resp=" +
- activityResponseCodeToString(responseCode));
- if (requestCode != RC_RESOLVE) {
- debugLog("onActivityResult: request code not meant for us. Ignoring.");
- return;
- }
- // no longer expecting a resolution
- mExpectingResolution = false;
- if (mState != STATE_CONNECTING) {
- debugLog("onActivityResult: ignoring because state isn't STATE_CONNECTING (" +
- "it's " + STATE_NAMES[mState] + ")");
- return;
- }
- // We're coming back from an activity that was launched to resolve a
- // connection problem. For example, the sign-in UI.
- if (responseCode == Activity.RESULT_OK) {
- // Ready to try to connect again.
- debugLog("onAR: Resolution was RESULT_OK, so connecting current client again.");
- connectCurrentClient();
- } else if (responseCode == GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED) {
- debugLog("onAR: Resolution was RECONNECT_REQUIRED, so reconnecting.");
- connectCurrentClient();
- } else if (responseCode == Activity.RESULT_CANCELED) {
- // User cancelled.
- debugLog("onAR: Got a cancellation result, so disconnecting.");
- mAutoSignIn = false;
- mUserInitiatedSignIn = false;
- mSignInFailureReason = null; // cancelling is not a failure!
- killConnections();
- notifyListener(false);
- } else {
- // Whatever the problem we were trying to solve, it was not
- // solved. So give up and show an error message.
- debugLog("onAR: responseCode=" + activityResponseCodeToString(responseCode) +
- ", so giving up.");
- giveUp(new SignInFailureReason(mConnectionResult.getErrorCode(), responseCode));
- }
- }
- void notifyListener(boolean success) {
- debugLog("Notifying LISTENER of sign-in " + (success ? "SUCCESS" :
- mSignInFailureReason != null ? "FAILURE (error)" : "FAILURE (no error)"));
- if (mListener != null) {
- if (success) {
- mListener.onSignInSucceeded();
- } else {
- mListener.onSignInFailed();
- }
- }
- }
- /**
- * Starts a user-initiated sign-in flow. This should be called when the user
- * clicks on a "Sign In" button. As a result, authentication/consent dialogs
- * may show up. At the end of the process, the GameHelperListener's
- * onSignInSucceeded() or onSignInFailed() methods will be called.
- */
- public void beginUserInitiatedSignIn() {
- if (mState == STATE_CONNECTED) {
- // nothing to do
- logWarn("beginUserInitiatedSignIn() called when already connected. " +
- "Calling listener directly to notify of success.");
- notifyListener(true);
- return;
- } else if (mState == STATE_CONNECTING) {
- logWarn("beginUserInitiatedSignIn() called when already connecting. " +
- "Be patient! You can only call this method after you get an " +
- "onSignInSucceeded() or onSignInFailed() callback. Suggestion: disable " +
- "the sign-in button on startup and also when it's clicked, and re-enable " +
- "when you get the callback.");
- // ignore call (listener will get a callback when the connection process finishes)
- return;
- }
- debugLog("Starting USER-INITIATED sign-in flow.");
- // sign in automatically on onStart()
- mAutoSignIn = true;
- // Is Google Play services available?
- int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getContext());
- debugLog("isGooglePlayServicesAvailable returned " + result);
- if (result != ConnectionResult.SUCCESS) {
- // Google Play services is not available.
- debugLog("Google Play services not available. Show error dialog.");
- mSignInFailureReason = new SignInFailureReason(result, 0);
- showFailureDialog();
- notifyListener(false);
- return;
- }
- // indicate that user is actively trying to sign in (so we know to resolve
- // connection problems by showing dialogs)
- mUserInitiatedSignIn = true;
- if (mConnectionResult != null) {
- // We have a pending connection result from a previous failure, so
- // start with that.
- debugLog("beginUserInitiatedSignIn: continuing pending sign-in flow.");
- setState(STATE_CONNECTING);
- resolveConnectionResult();
- } else {
- // We don't have a pending connection result, so start anew.
- debugLog("beginUserInitiatedSignIn: starting new sign-in flow.");
- startConnections();
- }
- }
- Context getContext() {
- return mActivity;
- }
- void addToScope(StringBuilder scopeStringBuilder, String scope) {
- if (scopeStringBuilder.length() == 0) {
- scopeStringBuilder.append("oauth2:");
- } else {
- scopeStringBuilder.append(" ");
- }
- scopeStringBuilder.append(scope);
- }
- void startConnections() {
- if (!checkState(TYPE_GAMEHELPER_BUG, "startConnections", "startConnections should " +
- "only get called when disconnected.", STATE_DISCONNECTED)) {
- return;
- }
- debugLog("Starting connections.");
- setState(STATE_CONNECTING);
- mInvitationId = null;
- connectNextClient();
- }
- void connectNextClient() {
- // do we already have all the clients we need?
- debugLog("connectNextClient: requested clients: " + mRequestedClients +
- ", connected clients: " + mConnectedClients);
- // failsafe, in case we somehow lost track of what clients are connected or not.
- if (mGamesClient != null && mGamesClient.isConnected() &&
- (0 == (mConnectedClients & CLIENT_GAMES))) {
- logWarn("GamesClient was already connected. Fixing.");
- mConnectedClients |= CLIENT_GAMES;
- }
- if (mPlusClient != null && mPlusClient.isConnected() &&
- (0 == (mConnectedClients & CLIENT_PLUS))) {
- logWarn("PlusClient was already connected. Fixing.");
- mConnectedClients |= CLIENT_PLUS;
- }
- if (mAppStateClient != null && mAppStateClient.isConnected() &&
- (0 == (mConnectedClients & CLIENT_APPSTATE))) {
- logWarn("AppStateClient was already connected. Fixing");
- mConnectedClients |= CLIENT_APPSTATE;
- }
- int pendingClients = mRequestedClients & ~mConnectedClients;
- debugLog("Pending clients: " + pendingClients);
- if (pendingClients == 0) {
- debugLog("All clients now connected. Sign-in successful!");
- succeedSignIn();
- return;
- }
- // which client should be the next one to connect?
- if (mGamesClient != null && (0 != (pendingClients & CLIENT_GAMES))) {
- debugLog("Connecting GamesClient.");
- mClientCurrentlyConnecting = CLIENT_GAMES;
- } else if (mPlusClient != null && (0 != (pendingClients & CLIENT_PLUS))) {
- debugLog("Connecting PlusClient.");
- mClientCurrentlyConnecting = CLIENT_PLUS;
- } else if (mAppStateClient != null && (0 != (pendingClients & CLIENT_APPSTATE))) {
- debugLog("Connecting AppStateClient.");
- mClientCurrentlyConnecting = CLIENT_APPSTATE;
- } else {
- // hmmm, getting here would be a bug.
- throw new AssertionError("Not all clients connected, yet no one is next. R="
- + mRequestedClients + ", C=" + mConnectedClients);
- }
- connectCurrentClient();
- }
- void connectCurrentClient() {
- if (mState == STATE_DISCONNECTED) {
- // we got disconnected during the connection process, so abort
- logWarn("GameHelper got disconnected during connection process. Aborting.");
- return;
- }
- if (!checkState(TYPE_GAMEHELPER_BUG, "connectCurrentClient", "connectCurrentClient " +
- "should only get called when connecting.", STATE_CONNECTING)) {
- return;
- }
- switch (mClientCurrentlyConnecting) {
- case CLIENT_GAMES:
- mGamesClient.connect();
- break;
- case CLIENT_APPSTATE:
- mAppStateClient.connect();
- break;
- case CLIENT_PLUS:
- mPlusClient.connect();
- break;
- }
- }
- /**
- * Disconnects the indicated clients, then connects them again.
- * @param whatClients Indicates which clients to reconnect.
- */
- public void reconnectClients(int whatClients) {
- checkState(TYPE_DEVELOPER_ERROR, "reconnectClients", "reconnectClients should " +
- "only be called when connected. Proceeding anyway.", STATE_CONNECTED);
- boolean actuallyReconnecting = false;
- if ((whatClients & CLIENT_GAMES) != 0 && mGamesClient != null
- && mGamesClient.isConnected()) {
- debugLog("Reconnecting GamesClient.");
- actuallyReconnecting = true;
- mConnectedClients &= ~CLIENT_GAMES;
- mGamesClient.reconnect();
- }
- if ((whatClients & CLIENT_APPSTATE) != 0 && mAppStateClient != null
- && mAppStateClient.isConnected()) {
- debugLog("Reconnecting AppStateClient.");
- actuallyReconnecting = true;
- mConnectedClients &= ~CLIENT_APPSTATE;
- mAppStateClient.reconnect();
- }
- if ((whatClients & CLIENT_PLUS) != 0 && mPlusClient != null
- && mPlusClient.isConnected()) {
- // PlusClient doesn't need reconnections.
- logWarn("GameHelper is ignoring your request to reconnect " +
- "PlusClient because this is unnecessary.");
- }
- if (actuallyReconnecting) {
- setState(STATE_CONNECTING);
- } else {
- // No reconnections are to take place, so for consistency we call the listener
- // as if sign in had just succeeded.
- debugLog("No reconnections needed, so behaving as if sign in just succeeded");
- notifyListener(true);
- }
- }
- /** Called when we successfully obtain a connection to a client. */
- @Override
- public void onConnected(Bundle connectionHint) {
- debugLog("onConnected: connected! client=" + mClientCurrentlyConnecting);
- // Mark the current client as connected
- mConnectedClients |= mClientCurrentlyConnecting;
- debugLog("Connected clients updated to: " + mConnectedClients);
- // If this was the games client and it came with an invite, store it for
- // later retrieval.
- if (mClientCurrentlyConnecting == CLIENT_GAMES && connectionHint != null) {
- debugLog("onConnected: connection hint provided. Checking for invite.");
- Invitation inv = connectionHint.getParcelable(GamesClient.EXTRA_INVITATION);
- if (inv != null && inv.getInvitationId() != null) {
- // accept invitation
- debugLog("onConnected: connection hint has a room invite!");
- mInvitationId = inv.getInvitationId();
- debugLog("Invitation ID: " + mInvitationId);
- }
- }
- // connect the next client in line, if any.
- connectNextClient();
- }
- void succeedSignIn() {
- checkState(TYPE_GAMEHELPER_BUG, "succeedSignIn", "succeedSignIn should only " +
- "get called in the connecting or connected state. Proceeding anyway.",
- STATE_CONNECTING, STATE_CONNECTED);
- debugLog("All requested clients connected. Sign-in succeeded!");
- setState(STATE_CONNECTED);
- mSignInFailureReason = null;
- mAutoSignIn = true;
- mUserInitiatedSignIn = false;
- notifyListener(true);
- }
- /** Handles a connection failure reported by a client. */
- @Override
- public void onConnectionFailed(ConnectionResult result) {
- // save connection result for later reference
- debugLog("onConnectionFailed");
- mConnectionResult = result;
- debugLog("Connection failure:");
- debugLog(" - code: " + errorCodeToString(mConnectionResult.getErrorCode()));
- debugLog(" - resolvable: " + mConnectionResult.hasResolution());
- debugLog(" - details: " + mConnectionResult.toString());
- if (!mUserInitiatedSignIn) {
- // If the user didn't initiate the sign-in, we don't try to resolve
- // the connection problem automatically -- instead, we fail and wait
- // for the user to want to sign in. That way, they won't get an
- // authentication (or other) popup unless they are actively trying
- // to
- // sign in.
- debugLog("onConnectionFailed: since user didn't initiate sign-in, failing now.");
- mConnectionResult = result;
- setState(STATE_DISCONNECTED);
- notifyListener(false);
- return;
- }
- debugLog("onConnectionFailed: since user initiated sign-in, resolving problem.");
- // Resolve the connection result. This usually means showing a dialog or
- // starting an Activity that will allow the user to give the appropriate
- // consents so that sign-in can be successful.
- resolveConnectionResult();
- }
- /**
- * Attempts to resolve a connection failure. This will usually involve
- * starting a UI flow that lets the user give the appropriate consents
- * necessary for sign-in to work.
- */
- void resolveConnectionResult() {
- // Try to resolve the problem
- checkState(TYPE_GAMEHELPER_BUG, "resolveConnectionResult",
- "resolveConnectionResult should only be called when connecting. Proceeding anyway.",
- STATE_CONNECTING);
- if (mExpectingResolution) {
- debugLog("We're already expecting the result of a previous resolution.");
- return;
- }
- debugLog("resolveConnectionResult: trying to resolve result: " + mConnectionResult);
- if (mConnectionResult.hasResolution()) {
- // This problem can be fixed. So let's try to fix it.
- debugLog("Result has resolution. Starting it.");
- try {
- // launch appropriate UI flow (which might, for example, be the
- // sign-in flow)
- mExpectingResolution = true;
- mConnectionResult.startResolutionForResult(mActivity, RC_RESOLVE);
- } catch (SendIntentException e) {
- // Try connecting again
- debugLog("SendIntentException, so connecting again.");
- connectCurrentClient();
- }
- } else {
- // It's not a problem what we can solve, so give up and show an
- // error.
- debugLog("resolveConnectionResult: result has no resolution. Giving up.");
- giveUp(new SignInFailureReason(mConnectionResult.getErrorCode()));
- }
- }
- /**
- * Give up on signing in due to an error. Shows the appropriate error
- * message to the user, using a standard error dialog as appropriate to the
- * cause of the error. That dialog will indicate to the user how the problem
- * can be solved (for example, re-enable Google Play Services, upgrade to a
- * new version, etc).
- */
- void giveUp(SignInFailureReason reason) {
- checkState(TYPE_GAMEHELPER_BUG, "giveUp", "giveUp should only be called when " +
- "connecting. Proceeding anyway.", STATE_CONNECTING);
- mAutoSignIn = false;
- killConnections();
- mSignInFailureReason = reason;
- showFailureDialog();
- notifyListener(false);
- }
- /** Called when we are disconnected from a client. */
- @Override
- public void onDisconnected() {
- debugLog("onDisconnected.");
- if (mState == STATE_DISCONNECTED) {
- // This is expected.
- debugLog("onDisconnected is expected, so no action taken.");
- return;
- }
- // Unexpected disconnect (rare!)
- logWarn("Unexpectedly disconnected. Severing remaining connections.");
- // kill the other connections too, and revert to DISCONNECTED state.
- killConnections();
- mSignInFailureReason = null;
- // call the sign in failure callback
- debugLog("Making extraordinary call to onSignInFailed callback");
- notifyListener(false);
- }
- /** Shows an error dialog that's appropriate for the failure reason. */
- void showFailureDialog() {
- Context ctx = getContext();
- if (ctx == null) {
- debugLog("*** No context. Can't show failure dialog.");
- return;
- }
- debugLog("Making error dialog for failure: " + mSignInFailureReason);
- Dialog errorDialog = null;
- int errorCode = mSignInFailureReason.getServiceErrorCode();
- int actResp = mSignInFailureReason.getActivityResultCode();
- switch (actResp) {
- case GamesActivityResultCodes.RESULT_APP_MISCONFIGURED:
- errorDialog = makeSimpleDialog(ctx.getString(
- R.string.gamehelper_app_misconfigured));
- printMisconfiguredDebugInfo();
- break;
- case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED:
- errorDialog = makeSimpleDialog(ctx.getString(
- R.string.gamehelper_sign_in_failed));
- break;
- case GamesActivityResultCodes.RESULT_LICENSE_FAILED:
- errorDialog = makeSimpleDialog(ctx.getString(
- R.string.gamehelper_license_failed));
- break;
- default:
- // No meaningful Activity response code, so generate default Google
- // Play services dialog
- errorDialog = GooglePlayServicesUtil.getErrorDialog(errorCode, mActivity,
- RC_UNUSED, null);
- if (errorDialog == null) {
- // get fallback dialog
- debugLog("No standard error dialog available. Making fallback dialog.");
- errorDialog = makeSimpleDialog(ctx.getString(R.string.gamehelper_unknown_error)
- + " " + errorCodeToString(errorCode));
- }
- }
- debugLog("Showing error dialog.");
- errorDialog.show();
- }
- Dialog makeSimpleDialog(String text) {
- return (new AlertDialog.Builder(getContext())).setMessage(text)
- .setNeutralButton(android.R.string.ok, null).create();
- }
- void debugLog(String message) {
- if (mDebugLog) {
- Log.d(mDebugTag, "GameHelper: " + message);
- }
- }
- void logWarn(String message) {
- Log.w(mDebugTag, "!!! GameHelper WARNING: " + message);
- }
- void logError(String message) {
- Log.e(mDebugTag, "*** GameHelper ERROR: " + message);
- }
- static String errorCodeToString(int errorCode) {
- switch (errorCode) {
- case ConnectionResult.DEVELOPER_ERROR:
- return "DEVELOPER_ERROR(" + errorCode + ")";
- case ConnectionResult.INTERNAL_ERROR:
- return "INTERNAL_ERROR(" + errorCode + ")";
- case ConnectionResult.INVALID_ACCOUNT:
- return "INVALID_ACCOUNT(" + errorCode + ")";
- case ConnectionResult.LICENSE_CHECK_FAILED:
- return "LICENSE_CHECK_FAILED(" + errorCode + ")";
- case ConnectionResult.NETWORK_ERROR:
- return "NETWORK_ERROR(" + errorCode + ")";
- case ConnectionResult.RESOLUTION_REQUIRED:
- return "RESOLUTION_REQUIRED(" + errorCode + ")";
- case ConnectionResult.SERVICE_DISABLED:
- return "SERVICE_DISABLED(" + errorCode + ")";
- case ConnectionResult.SERVICE_INVALID:
- return "SERVICE_INVALID(" + errorCode + ")";
- case ConnectionResult.SERVICE_MISSING:
- return "SERVICE_MISSING(" + errorCode + ")";
- case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
- return "SERVICE_VERSION_UPDATE_REQUIRED(" + errorCode + ")";
- case ConnectionResult.SIGN_IN_REQUIRED:
- return "SIGN_IN_REQUIRED(" + errorCode + ")";
- case ConnectionResult.SUCCESS:
- return "SUCCESS(" + errorCode + ")";
- default:
- return "Unknown error code " + errorCode;
- }
- }
- // Represents the reason for a sign-in failure
- public static class SignInFailureReason {
- public static final int NO_ACTIVITY_RESULT_CODE = -100;
- int mServiceErrorCode = 0;
- int mActivityResultCode = NO_ACTIVITY_RESULT_CODE;
- public int getServiceErrorCode() {
- return mServiceErrorCode;
- }
- public int getActivityResultCode() {
- return mActivityResultCode;
- }
- public SignInFailureReason(int serviceErrorCode, int activityResultCode) {
- mServiceErrorCode = serviceErrorCode;
- mActivityResultCode = activityResultCode;
- }
- public SignInFailureReason(int serviceErrorCode) {
- this(serviceErrorCode, NO_ACTIVITY_RESULT_CODE);
- }
- @Override
- public String toString() {
- return "SignInFailureReason(serviceErrorCode:" +
- errorCodeToString(mServiceErrorCode) +
- ((mActivityResultCode == NO_ACTIVITY_RESULT_CODE) ? ")" :
- (",activityResultCode:" +
- activityResponseCodeToString(mActivityResultCode) + ")"));
- }
- }
- void printMisconfiguredDebugInfo() {
- debugLog("****");
- debugLog("****");
- debugLog("**** APP NOT CORRECTLY CONFIGURED TO USE GOOGLE PLAY GAME SERVICES");
- debugLog("**** This is usually caused by one of these reasons:");
- debugLog("**** (1) Your package name and certificate fingerprint do not match");
- debugLog("**** the client ID you registered in Developer Console.");
- debugLog("**** (2) Your App ID was incorrectly entered.");
- debugLog("**** (3) Your game settings have not been published and you are ");
- debugLog("**** trying to log in with an account that is not listed as");
- debugLog("**** a test account.");
- debugLog("****");
- Context ctx = getContext();
- if (ctx == null) {
- debugLog("*** (no Context, so can't print more debug info)");
- return;
- }
- debugLog("**** To help you debug, here is the information about this app");
- debugLog("**** Package name : " + getContext().getPackageName());
- debugLog("**** Cert SHA1 fingerprint: " + getSHA1CertFingerprint());
- debugLog("**** App ID from : " + getAppIdFromResource());
- debugLog("****");
- debugLog("**** Check that the above information matches your setup in ");
- debugLog("**** Developer Console. Also, check that you're logging in with the");
- debugLog("**** right account (it should be listed in the Testers section if");
- debugLog("**** your project is not yet published).");
- debugLog("****");
- debugLog("**** For more information, refer to the troubleshooting guide:");
- debugLog("**** http://developers.google.com/games/services/android/troubleshooting");
- }
- String getAppIdFromResource() {
- try {
- Resources res = getContext().getResources();
- String pkgName = getContext().getPackageName();
- int res_id = res.getIdentifier("app_id", "string", pkgName);
- return res.getString(res_id);
- } catch (Exception ex) {
- ex.printStackTrace();
- return "??? (failed to retrieve APP ID)";
- }
- }
- String getSHA1CertFingerprint() {
- try {
- Signature[] sigs = getContext().getPackageManager().getPackageInfo(
- getContext().getPackageName(), PackageManager.GET_SIGNATURES).signatures;
- if (sigs.length == 0) {
- return "ERROR: NO SIGNATURE.";
- } else if (sigs.length > 1) {
- return "ERROR: MULTIPLE SIGNATURES";
- }
- byte[] digest = MessageDigest.getInstance("SHA1").digest(sigs[0].toByteArray());
- StringBuilder hexString = new StringBuilder();
- for (int i = 0; i < digest.length; ++i) {
- if (i > 0) {
- hexString.append(":");
- }
- byteToString(hexString, digest[i]);
- }
- return hexString.toString();
- } catch (PackageManager.NameNotFoundException ex) {
- ex.printStackTrace();
- return "(ERROR: package not found)";
- } catch (NoSuchAlgorithmException ex) {
- ex.printStackTrace();
- return "(ERROR: SHA1 algorithm not found)";
- }
- }
- void byteToString(StringBuilder sb, byte b) {
- int unsigned_byte = b < 0 ? b + 256 : b;
- int hi = unsigned_byte / 16;
- int lo = unsigned_byte % 16;
- sb.append("0123456789ABCDEF".substring(hi, hi + 1));
- sb.append("0123456789ABCDEF".substring(lo, lo + 1));
- }
- }
|