1061 lines
39 KiB
Java
1061 lines
39 KiB
Java
/*
|
|
* 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 com.google.example.games.basegameutils;
|
|
|
|
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.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.drive.Drive;
|
|
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_SNAPSHOT = 0x05;
|
|
public final static int CLIENT_ALL = CLIENT_GAMES //| CLIENT_PLUS
|
|
| CLIENT_APPSTATE | CLIENT_SNAPSHOT;
|
|
|
|
// 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);
|
|
// builder.addScope(Plus.SCOPE_PLUS_LOGIN);
|
|
//}
|
|
|
|
if (0 != (mRequestedClients & CLIENT_SNAPSHOT)) {
|
|
builder.addScope(Drive.SCOPE_APPFOLDER);
|
|
builder.addApi(Drive.API);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|