Back to project page MorseWithPals.
The source code is released under:
MIT License
If you think the Android project MorseWithPals listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.
/* * Copyright (C) 2013 Google Inc./*from ww w . j a va 2s .com*/ * * 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 com.google.example.games.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.games.multiplayer.turnbased.TurnBasedMatch; 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; /* * If we got turn-based match when we connected to the games client, it's here. Otherwise, it's * null. */ TurnBasedMatch mTurnBasedMatch; // 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"); // If you want to suppress the signin interstitial, set setShowConnectingPopup to false. mGamesClient = new GamesClient.Builder(getContext(), this, this) .setGravityForPopups(Gravity.TOP | Gravity.CENTER_HORIZONTAL) .setScopes(mScopes) .setShowConnectingPopup(true) .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; } /** * Returns the tbmp match received through an invitation notification. This * should be called from your GameHelperListener's * @link{GameHelperListener#onSignInSucceeded} method, to check if there's a * match available. * @return The match, or null if none was received. */ public TurnBasedMatch getTurnBasedMatch() { if (!checkState(TYPE_DEVELOPER_ERROR, "getTurnBasedMatch", "TurnBasedMatch is only available when connected " + "(after getting the onSignInSucceeded callback).", STATE_CONNECTED)) { return null; } return mTurnBasedMatch; } /** 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) { // Don't retain references to old matches. mTurnBasedMatch = null; 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); } debugLog("onConnected: connection hint provided. Checking for TBMP game."); TurnBasedMatch match = connectionHint .getParcelable(GamesClient.EXTRA_TURN_BASED_MATCH); if (match != null) { mTurnBasedMatch = match; } } // 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)); } }