Last active
August 29, 2015 14:01
-
-
Save EmmanuelVinas/ef09a35bcc805ba6deb3 to your computer and use it in GitHub Desktop.
GameHelper using game services 4.4
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.ArrayList; | |
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.SharedPreferences; | |
import android.os.Bundle; | |
import android.os.Handler; | |
import android.util.Log; | |
import com.google.android.gms.appstate.AppStateManager; | |
import com.google.android.gms.common.ConnectionResult; | |
import com.google.android.gms.common.GooglePlayServicesUtil; | |
import com.google.android.gms.common.api.Api.ApiOptions.NoOptions; | |
import com.google.android.gms.common.api.GoogleApiClient; | |
import com.google.android.gms.games.Games; | |
import com.google.android.gms.games.Games.GamesOptions; | |
import com.google.android.gms.games.GamesActivityResultCodes; | |
import com.google.android.gms.games.multiplayer.Invitation; | |
import com.google.android.gms.games.multiplayer.Multiplayer; | |
import com.google.android.gms.games.multiplayer.turnbased.TurnBasedMatch; | |
import com.google.android.gms.games.request.GameRequest; | |
import com.google.android.gms.plus.Plus; | |
import com.google.android.gms.plus.Plus.PlusOptions; | |
public class GameHelper implements GoogleApiClient.ConnectionCallbacks, | |
GoogleApiClient.OnConnectionFailedListener { | |
static final String TAG = "GameHelper"; | |
/** 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(); | |
} | |
// configuration done? | |
private boolean mSetupDone = false; | |
// are we currently connecting? | |
private boolean mConnecting = false; | |
// Are we expecting the result of a resolution flow? | |
boolean mExpectingResolution = false; | |
// was the sign-in flow cancelled when we tried it? | |
// if true, we know not to try again automatically. | |
boolean mSignInCancelled = 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; | |
// app context | |
Context mAppContext = null; | |
// 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; | |
// the Google API client builder we will use to create GoogleApiClient | |
GoogleApiClient.Builder mGoogleApiClientBuilder = null; | |
// Api options to use when adding each API, null for none | |
GamesOptions mGamesApiOptions = GamesOptions.builder().build(); | |
PlusOptions mPlusApiOptions = null; | |
NoOptions mAppStateApiOptions = null; | |
// Google API client object we manage. | |
GoogleApiClient mGoogleApiClient = null; | |
// Client request 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; | |
// Whether to automatically try to sign in on onStart(). We only set this | |
// to true when the sign-in process fails or the user explicitly signs out. | |
// We set it back to false when the user initiates the sign in process. | |
boolean mConnectOnStart = 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; | |
// Should we show error dialog boxes? | |
boolean mShowErrorDialogs = true; | |
// Print debug logs? | |
boolean mDebugLog = false; | |
Handler mHandler; | |
/* | |
* If we got an invitation when we connected to the games client, it's here. | |
* Otherwise, it's null. | |
*/ | |
Invitation mInvitation; | |
/* | |
* If we got turn-based match when we connected to the games client, it's | |
* here. Otherwise, it's null. | |
*/ | |
TurnBasedMatch mTurnBasedMatch; | |
/* | |
* If we have incoming requests when we connected to the games client, they | |
* are here. Otherwise, it's null. | |
*/ | |
ArrayList<GameRequest> mRequests; | |
// Listener | |
GameHelperListener mListener = null; | |
// Should we start the flow to sign the user in automatically on startup? If | |
// so, up to | |
// how many times in the life of the application? | |
static final int DEFAULT_MAX_SIGN_IN_ATTEMPTS = 3; | |
int mMaxAutoSignInAttempts = DEFAULT_MAX_SIGN_IN_ATTEMPTS; | |
/** | |
* Construct a GameHelper object, initially tied to the given Activity. | |
* After constructing this object, call @link{setup} from the onCreate() | |
* method of your Activity. | |
* | |
* @param clientsToUse | |
* the API clients to use (a combination of the CLIENT_* flags, | |
* or CLIENT_ALL to mean all clients). | |
*/ | |
public GameHelper(Activity activity, int clientsToUse) { | |
mActivity = activity; | |
mAppContext = activity.getApplicationContext(); | |
mRequestedClients = clientsToUse; | |
mHandler = new Handler(); | |
} | |
/** | |
* Sets the maximum number of automatic sign-in attempts to be made on | |
* application startup. This maximum is over the lifetime of the application | |
* (it is stored in a SharedPreferences file). So, for example, if you | |
* specify 2, then it means that the user will be prompted to sign in on app | |
* startup the first time and, if they cancel, a second time the next time | |
* the app starts, and, if they cancel that one, never again. Set to 0 if | |
* you do not want the user to be prompted to sign in on application | |
* startup. | |
*/ | |
public void setMaxAutoSignInAttempts(int max) { | |
mMaxAutoSignInAttempts = max; | |
} | |
void assertConfigured(String operation) { | |
if (!mSetupDone) { | |
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); | |
} | |
} | |
private void doApiOptionsPreCheck() { | |
if (mGoogleApiClientBuilder != null) { | |
String error = "GameHelper: you cannot call set*ApiOptions after the client " | |
+ "builder has been created. Call it before calling createApiClientBuilder() " | |
+ "or setup()."; | |
logError(error); | |
throw new IllegalStateException(error); | |
} | |
} | |
/** | |
* Sets the options to pass when setting up the Games API. Call before | |
* setup(). | |
*/ | |
public void setGamesApiOptions(GamesOptions options) { | |
doApiOptionsPreCheck(); | |
mGamesApiOptions = options; | |
} | |
/** | |
* Sets the options to pass when setting up the AppState API. Call before | |
* setup(). | |
*/ | |
public void setAppStateApiOptions(NoOptions options) { | |
doApiOptionsPreCheck(); | |
mAppStateApiOptions = options; | |
} | |
/** | |
* Sets the options to pass when setting up the Plus API. Call before | |
* setup(). | |
*/ | |
public void setPlusApiOptions(PlusOptions options) { | |
doApiOptionsPreCheck(); | |
mPlusApiOptions = options; | |
} | |
/** | |
* Creates a GoogleApiClient.Builder for use with @link{#setup}. Normally, | |
* you do not have to do this; use this method only if you need to make | |
* nonstandard setup (e.g. adding extra scopes for other APIs) on the | |
* GoogleApiClient.Builder before calling @link{#setup}. | |
*/ | |
public GoogleApiClient.Builder createApiClientBuilder() { | |
if (mSetupDone) { | |
String error = "GameHelper: you called GameHelper.createApiClientBuilder() after " | |
+ "calling setup. You can only get a client builder BEFORE performing setup."; | |
logError(error); | |
throw new IllegalStateException(error); | |
} | |
GoogleApiClient.Builder builder = new GoogleApiClient.Builder( | |
mActivity, this, this); | |
if (0 != (mRequestedClients & CLIENT_GAMES)) { | |
builder.addApi(Games.API, mGamesApiOptions); | |
builder.addScope(Games.SCOPE_GAMES); | |
} | |
if (0 != (mRequestedClients & CLIENT_PLUS)) { | |
builder.addApi(Plus.API, mPlusApiOptions); | |
builder.addScope(Plus.SCOPE_PLUS_LOGIN); | |
} | |
if (0 != (mRequestedClients & CLIENT_APPSTATE)) { | |
builder.addApi(AppStateManager.API); | |
builder.addScope(AppStateManager.SCOPE_APP_STATE); | |
} | |
mGoogleApiClientBuilder = builder; | |
return builder; | |
} | |
/** | |
* 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. | |
*/ | |
public void setup(GameHelperListener listener) { | |
if (mSetupDone) { | |
String error = "GameHelper: you cannot call GameHelper.setup() more than once!"; | |
logError(error); | |
throw new IllegalStateException(error); | |
} | |
mListener = listener; | |
debugLog("Setup: requested clients: " + mRequestedClients); | |
if (mGoogleApiClientBuilder == null) { | |
// we don't have a builder yet, so create one | |
createApiClientBuilder(); | |
} | |
mGoogleApiClient = mGoogleApiClientBuilder.build(); | |
mGoogleApiClientBuilder = null; | |
mSetupDone = true; | |
} | |
/** | |
* Returns the GoogleApiClient object. In order to call this method, you | |
* must have called @link{setup}. | |
*/ | |
public GoogleApiClient getApiClient() { | |
if (mGoogleApiClient == null) { | |
throw new IllegalStateException( | |
"No GoogleApiClient. Did you call setup()?"); | |
} | |
return mGoogleApiClient; | |
} | |
/** Returns whether or not the user is signed in. */ | |
public boolean isSignedIn() { | |
return mGoogleApiClient != null && mGoogleApiClient.isConnected(); | |
} | |
/** Returns whether or not we are currently connecting */ | |
public boolean isConnecting() { | |
return mConnecting; | |
} | |
/** | |
* 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; | |
} | |
// Set whether to show error dialogs or not. | |
public void setShowErrorDialogs(boolean show) { | |
mShowErrorDialogs = show; | |
} | |
/** Call this method from your Activity's onStart(). */ | |
public void onStart(Activity act) { | |
mActivity = act; | |
mAppContext = act.getApplicationContext(); | |
debugLog("onStart"); | |
assertConfigured("onStart"); | |
if (mConnectOnStart) { | |
if (mGoogleApiClient.isConnected()) { | |
Log.w(TAG, | |
"GameHelper: client was already connected on onStart()"); | |
} else { | |
debugLog("Connecting client."); | |
mConnecting = true; | |
mGoogleApiClient.connect(); | |
} | |
} else { | |
debugLog("Not attempting to connect becase mConnectOnStart=false"); | |
debugLog("Instead, reporting a sign-in failure."); | |
mHandler.postDelayed(new Runnable() { | |
@Override | |
public void run() { | |
notifyListener(false); | |
} | |
}, 1000); | |
} | |
} | |
/** Call this method from your Activity's onStop(). */ | |
public void onStop() { | |
debugLog("onStop"); | |
assertConfigured("onStop"); | |
if (mGoogleApiClient.isConnected()) { | |
debugLog("Disconnecting client due to onStop"); | |
mGoogleApiClient.disconnect(); | |
} else { | |
debugLog("Client already disconnected when we got onStop."); | |
} | |
mConnecting = false; | |
mExpectingResolution = false; | |
// let go of the Activity reference | |
mActivity = null; | |
} | |
/** | |
* 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 (!mGoogleApiClient.isConnected()) { | |
Log.w(TAG, | |
"Warning: getInvitationId() should only be called when signed in, " | |
+ "that is, after getting onSignInSuceeded()"); | |
} | |
return mInvitation == null ? null : mInvitation.getInvitationId(); | |
} | |
/** | |
* Returns the invitation 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 invitation, or null if none was received. | |
*/ | |
public Invitation getInvitation() { | |
if (!mGoogleApiClient.isConnected()) { | |
Log.w(TAG, | |
"Warning: getInvitation() should only be called when signed in, " | |
+ "that is, after getting onSignInSuceeded()"); | |
} | |
return mInvitation; | |
} | |
public boolean hasInvitation() { | |
return mInvitation != null; | |
} | |
public boolean hasTurnBasedMatch() { | |
return mTurnBasedMatch != null; | |
} | |
public boolean hasRequests() { | |
return mRequests != null; | |
} | |
public void clearInvitation() { | |
mInvitation = null; | |
} | |
public void clearTurnBasedMatch() { | |
mTurnBasedMatch = null; | |
} | |
public void clearRequests() { | |
mRequests = null; | |
} | |
/** | |
* 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 (!mGoogleApiClient.isConnected()) { | |
Log.w(TAG, | |
"Warning: getTurnBasedMatch() should only be called when signed in, " | |
+ "that is, after getting onSignInSuceeded()"); | |
} | |
return mTurnBasedMatch; | |
} | |
/** | |
* Returns the requests received through the onConnected bundle. This should | |
* be called from your GameHelperListener's | |
* | |
* @link{GameHelperListener#onSignInSucceeded method, to check if there are | |
* incoming requests that must be | |
* handled. | |
* @return The requests, or null if none were received. | |
*/ | |
public ArrayList<GameRequest> getRequests() { | |
if (!mGoogleApiClient.isConnected()) { | |
Log.w(TAG, "Warning: getRequests() should only be called " | |
+ "when signed in, " | |
+ "that is, after getting onSignInSuceeded()"); | |
} | |
return mRequests; | |
} | |
/** Enables debug logging */ | |
public void enableDebugLog(boolean enabled) { | |
mDebugLog = enabled; | |
if (enabled) { | |
debugLog("Debug log enabled."); | |
} | |
} | |
@Deprecated | |
public void enableDebugLog(boolean enabled, String tag) { | |
Log.w(TAG, "GameHelper.enableDebugLog(boolean,String) is deprecated. " | |
+ "Use GameHelper.enableDebugLog(boolean)"); | |
enableDebugLog(enabled); | |
} | |
/** Sign out and disconnect from the APIs. */ | |
public void signOut() { | |
if (!mGoogleApiClient.isConnected()) { | |
// nothing to do | |
debugLog("signOut: was already disconnected, ignoring."); | |
return; | |
} | |
// for Plus, "signing out" means clearing the default account and | |
// then disconnecting | |
if (0 != (mRequestedClients & CLIENT_PLUS)) { | |
debugLog("Clearing default account on PlusClient."); | |
Plus.AccountApi.clearDefaultAccount(mGoogleApiClient); | |
} | |
// For the games client, signing out means calling signOut and | |
// disconnecting | |
if (0 != (mRequestedClients & CLIENT_GAMES)) { | |
debugLog("Signing out from the Google API Client."); | |
Games.signOut(mGoogleApiClient); | |
} | |
// Ready to disconnect | |
debugLog("Disconnecting client."); | |
mConnectOnStart = false; | |
mConnecting = false; | |
mGoogleApiClient.disconnect(); | |
} | |
/** | |
* 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=" | |
+ GameHelperUtils.activityResponseCodeToString(responseCode)); | |
if (requestCode != RC_RESOLVE) { | |
debugLog("onActivityResult: request code not meant for us. Ignoring."); | |
return; | |
} | |
// no longer expecting a resolution | |
mExpectingResolution = false; | |
if (!mConnecting) { | |
debugLog("onActivityResult: ignoring because we are not connecting."); | |
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."); | |
connect(); | |
} else if (responseCode == GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED) { | |
debugLog("onAR: Resolution was RECONNECT_REQUIRED, so reconnecting."); | |
connect(); | |
} else if (responseCode == Activity.RESULT_CANCELED) { | |
// User cancelled. | |
debugLog("onAR: Got a cancellation result, so disconnecting."); | |
mSignInCancelled = true; | |
mConnectOnStart = false; | |
mUserInitiatedSignIn = false; | |
mSignInFailureReason = null; // cancelling is not a failure! | |
mConnecting = false; | |
mGoogleApiClient.disconnect(); | |
// increment # of cancellations | |
int prevCancellations = getSignInCancellations(); | |
int newCancellations = incrementSignInCancellations(); | |
debugLog("onAR: # of cancellations " + prevCancellations + " --> " | |
+ newCancellations + ", max " + mMaxAutoSignInAttempts); | |
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=" | |
+ GameHelperUtils | |
.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() { | |
debugLog("beginUserInitiatedSignIn: resetting attempt count."); | |
resetSignInCancellations(); | |
mSignInCancelled = false; | |
mConnectOnStart = true; | |
if (mGoogleApiClient.isConnected()) { | |
// nothing to do | |
logWarn("beginUserInitiatedSignIn() called when already connected. " | |
+ "Calling listener directly to notify of success."); | |
notifyListener(true); | |
return; | |
} else if (mConnecting) { | |
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."); | |
// 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."); | |
mConnecting = true; | |
resolveConnectionResult(); | |
} else { | |
// We don't have a pending connection result, so start anew. | |
debugLog("beginUserInitiatedSignIn: starting new sign-in flow."); | |
mConnecting = true; | |
connect(); | |
} | |
} | |
void connect() { | |
if (mGoogleApiClient.isConnected()) { | |
debugLog("Already connected."); | |
return; | |
} | |
debugLog("Starting connection."); | |
mConnecting = true; | |
mInvitation = null; | |
mTurnBasedMatch = null; | |
mGoogleApiClient.connect(); | |
} | |
/** | |
* Disconnects the API client, then connects again. | |
*/ | |
public void reconnectClient() { | |
if (!mGoogleApiClient.isConnected()) { | |
Log.w(TAG, "reconnectClient() called when client is not connected."); | |
// interpret it as a request to connect | |
connect(); | |
} else { | |
debugLog("Reconnecting client."); | |
mGoogleApiClient.reconnect(); | |
} | |
} | |
/** Called when we successfully obtain a connection to a client. */ | |
@Override | |
public void onConnected(Bundle connectionHint) { | |
debugLog("onConnected: connected!"); | |
if (connectionHint != null) { | |
debugLog("onConnected: connection hint provided. Checking for invite."); | |
Invitation inv = connectionHint | |
.getParcelable(Multiplayer.EXTRA_INVITATION); | |
if (inv != null && inv.getInvitationId() != null) { | |
// retrieve and cache the invitation ID | |
debugLog("onConnected: connection hint has a room invite!"); | |
mInvitation = inv; | |
debugLog("Invitation ID: " + mInvitation.getInvitationId()); | |
} | |
// Do we have any requests pending? | |
mRequests = Games.Requests | |
.getGameRequestsFromBundle(connectionHint); | |
if (!mRequests.isEmpty()) { | |
// We have requests in onConnected's connectionHint. | |
debugLog("onConnected: connection hint has " + mRequests.size() | |
+ " request(s)"); | |
} | |
debugLog("onConnected: connection hint provided. Checking for TBMP game."); | |
mTurnBasedMatch = connectionHint | |
.getParcelable(Multiplayer.EXTRA_TURN_BASED_MATCH); | |
} | |
// we're good to go | |
succeedSignIn(); | |
} | |
void succeedSignIn() { | |
debugLog("succeedSignIn"); | |
mSignInFailureReason = null; | |
mConnectOnStart = true; | |
mUserInitiatedSignIn = false; | |
mConnecting = false; | |
notifyListener(true); | |
} | |
private final String GAMEHELPER_SHARED_PREFS = "GAMEHELPER_SHARED_PREFS"; | |
private final String KEY_SIGN_IN_CANCELLATIONS = "KEY_SIGN_IN_CANCELLATIONS"; | |
// Return the number of times the user has cancelled the sign-in flow in the | |
// life of the app | |
int getSignInCancellations() { | |
SharedPreferences sp = mAppContext.getSharedPreferences( | |
GAMEHELPER_SHARED_PREFS, Context.MODE_PRIVATE); | |
return sp.getInt(KEY_SIGN_IN_CANCELLATIONS, 0); | |
} | |
// Increments the counter that indicates how many times the user has | |
// cancelled the sign in | |
// flow in the life of the application | |
int incrementSignInCancellations() { | |
int cancellations = getSignInCancellations(); | |
SharedPreferences.Editor editor = mAppContext.getSharedPreferences( | |
GAMEHELPER_SHARED_PREFS, Context.MODE_PRIVATE).edit(); | |
editor.putInt(KEY_SIGN_IN_CANCELLATIONS, cancellations + 1); | |
editor.commit(); | |
return cancellations + 1; | |
} | |
// Reset the counter of how many times the user has cancelled the sign-in | |
// flow. | |
void resetSignInCancellations() { | |
SharedPreferences.Editor editor = mAppContext.getSharedPreferences( | |
GAMEHELPER_SHARED_PREFS, Context.MODE_PRIVATE).edit(); | |
editor.putInt(KEY_SIGN_IN_CANCELLATIONS, 0); | |
editor.commit(); | |
} | |
/** Handles a connection failure. */ | |
@Override | |
public void onConnectionFailed(ConnectionResult result) { | |
// save connection result for later reference | |
debugLog("onConnectionFailed"); | |
mConnectionResult = result; | |
debugLog("Connection failure:"); | |
debugLog(" - code: " | |
+ GameHelperUtils.errorCodeToString(mConnectionResult | |
.getErrorCode())); | |
debugLog(" - resolvable: " + mConnectionResult.hasResolution()); | |
debugLog(" - details: " + mConnectionResult.toString()); | |
int cancellations = getSignInCancellations(); | |
boolean shouldResolve = false; | |
if (mUserInitiatedSignIn) { | |
debugLog("onConnectionFailed: WILL resolve because user initiated sign-in."); | |
shouldResolve = true; | |
} else if (mSignInCancelled) { | |
debugLog("onConnectionFailed WILL NOT resolve (user already cancelled once)."); | |
shouldResolve = false; | |
} else if (cancellations < mMaxAutoSignInAttempts) { | |
debugLog("onConnectionFailed: WILL resolve because we have below the max# of " | |
+ "attempts, " | |
+ cancellations | |
+ " < " | |
+ mMaxAutoSignInAttempts); | |
shouldResolve = true; | |
} else { | |
shouldResolve = false; | |
debugLog("onConnectionFailed: Will NOT resolve; not user-initiated and max attempts " | |
+ "reached: " | |
+ cancellations | |
+ " >= " | |
+ mMaxAutoSignInAttempts); | |
} | |
if (!shouldResolve) { | |
// Fail and wait for the user to want to sign in. | |
debugLog("onConnectionFailed: since we won't resolve, failing now."); | |
mConnectionResult = result; | |
mConnecting = false; | |
notifyListener(false); | |
return; | |
} | |
debugLog("onConnectionFailed: 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 | |
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."); | |
connect(); | |
} | |
} 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())); | |
} | |
} | |
public void disconnect() { | |
if (mGoogleApiClient.isConnected()) { | |
debugLog("Disconnecting client."); | |
mGoogleApiClient.disconnect(); | |
} else { | |
Log.w(TAG, | |
"disconnect() called when client was already disconnected."); | |
} | |
} | |
/** | |
* 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) { | |
mConnectOnStart = false; | |
disconnect(); | |
mSignInFailureReason = reason; | |
if (reason.mActivityResultCode == GamesActivityResultCodes.RESULT_APP_MISCONFIGURED) { | |
// print debug info for the developer | |
GameHelperUtils.printMisconfiguredDebugInfo(mAppContext); | |
} | |
showFailureDialog(); | |
mConnecting = false; | |
notifyListener(false); | |
} | |
/** Called when we are disconnected from the Google API client. */ | |
@Override | |
public void onConnectionSuspended(int cause) { | |
debugLog("onConnectionSuspended, cause=" + cause); | |
disconnect(); | |
mSignInFailureReason = null; | |
debugLog("Making extraordinary call to onSignInFailed callback"); | |
mConnecting = false; | |
notifyListener(false); | |
} | |
public void showFailureDialog() { | |
if (mSignInFailureReason != null) { | |
int errorCode = mSignInFailureReason.getServiceErrorCode(); | |
int actResp = mSignInFailureReason.getActivityResultCode(); | |
if (mShowErrorDialogs) { | |
showFailureDialog(mActivity, actResp, errorCode); | |
} else { | |
debugLog("Not showing error dialog because mShowErrorDialogs==false. " | |
+ "" + "Error was: " + mSignInFailureReason); | |
} | |
} | |
} | |
/** Shows an error dialog that's appropriate for the failure reason. */ | |
public static void showFailureDialog(Activity activity, int actResp, | |
int errorCode) { | |
if (activity == null) { | |
Log.e("GameHelper", "*** No Activity. Can't show failure dialog!"); | |
return; | |
} | |
Dialog errorDialog = null; | |
switch (actResp) { | |
case GamesActivityResultCodes.RESULT_APP_MISCONFIGURED: | |
errorDialog = makeSimpleDialog(activity, GameHelperUtils.getString( | |
activity, GameHelperUtils.R_APP_MISCONFIGURED)); | |
break; | |
case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED: | |
errorDialog = makeSimpleDialog(activity, GameHelperUtils.getString( | |
activity, GameHelperUtils.R_SIGN_IN_FAILED)); | |
break; | |
case GamesActivityResultCodes.RESULT_LICENSE_FAILED: | |
errorDialog = makeSimpleDialog(activity, GameHelperUtils.getString( | |
activity, GameHelperUtils.R_LICENSE_FAILED)); | |
break; | |
default: | |
// No meaningful Activity response code, so generate default Google | |
// Play services dialog | |
errorDialog = GooglePlayServicesUtil.getErrorDialog(errorCode, | |
activity, RC_UNUSED, null); | |
if (errorDialog == null) { | |
// get fallback dialog | |
Log.e("GameHelper", | |
"No standard error dialog available. Making fallback dialog."); | |
errorDialog = makeSimpleDialog( | |
activity, | |
GameHelperUtils.getString(activity, | |
GameHelperUtils.R_UNKNOWN_ERROR) | |
+ " " | |
+ GameHelperUtils.errorCodeToString(errorCode)); | |
} | |
} | |
errorDialog.show(); | |
} | |
static Dialog makeSimpleDialog(Activity activity, String text) { | |
return (new AlertDialog.Builder(activity)).setMessage(text) | |
.setNeutralButton(android.R.string.ok, null).create(); | |
} | |
static Dialog | |
makeSimpleDialog(Activity activity, String title, String text) { | |
return (new AlertDialog.Builder(activity)).setMessage(text) | |
.setTitle(title).setNeutralButton(android.R.string.ok, null) | |
.create(); | |
} | |
public Dialog makeSimpleDialog(String text) { | |
if (mActivity == null) { | |
logError("*** makeSimpleDialog failed: no current Activity!"); | |
return null; | |
} | |
return makeSimpleDialog(mActivity, text); | |
} | |
public Dialog makeSimpleDialog(String title, String text) { | |
if (mActivity == null) { | |
logError("*** makeSimpleDialog failed: no current Activity!"); | |
return null; | |
} | |
return makeSimpleDialog(mActivity, title, text); | |
} | |
void debugLog(String message) { | |
if (mDebugLog) { | |
Log.d(TAG, "GameHelper: " + message); | |
} | |
} | |
void logWarn(String message) { | |
Log.w(TAG, "!!! GameHelper WARNING: " + message); | |
} | |
void logError(String message) { | |
Log.e(TAG, "*** GameHelper ERROR: " + message); | |
} | |
// 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:" | |
+ GameHelperUtils.errorCodeToString(mServiceErrorCode) | |
+ ((mActivityResultCode == NO_ACTIVITY_RESULT_CODE) ? ")" | |
: (",activityResultCode:" | |
+ GameHelperUtils | |
.activityResponseCodeToString(mActivityResultCode) + ")")); | |
} | |
} | |
// Not recommended for general use. This method forces the | |
// "connect on start" flag | |
// to a given state. This may be useful when using GameHelper in a | |
// non-standard | |
// sign-in flow. | |
public void setConnectOnStart(boolean connectOnStart) { | |
debugLog("Forcing mConnectOnStart=" + connectOnStart); | |
mConnectOnStart = connectOnStart; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment