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