GameHelper.java 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. /*
  2. * Copyright (C) 2013 Google Inc.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. package org.gameplay3d.sample_spaceship.basegameutils;
  17. import java.security.MessageDigest;
  18. import java.security.NoSuchAlgorithmException;
  19. import java.util.Vector;
  20. import android.app.Activity;
  21. import android.app.AlertDialog;
  22. import android.app.Dialog;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.content.IntentSender.SendIntentException;
  26. import android.content.pm.PackageManager;
  27. import android.content.pm.Signature;
  28. import android.content.res.Resources;
  29. import android.os.Bundle;
  30. import android.util.Log;
  31. import android.view.Gravity;
  32. import com.google.android.gms.appstate.AppStateClient;
  33. import com.google.android.gms.common.ConnectionResult;
  34. import com.google.android.gms.common.GooglePlayServicesClient;
  35. import com.google.android.gms.common.GooglePlayServicesUtil;
  36. import com.google.android.gms.common.Scopes;
  37. import com.google.android.gms.games.GamesActivityResultCodes;
  38. import com.google.android.gms.games.GamesClient;
  39. import com.google.android.gms.games.multiplayer.Invitation;
  40. import com.google.android.gms.plus.PlusClient;
  41. public class GameHelper implements GooglePlayServicesClient.ConnectionCallbacks,
  42. GooglePlayServicesClient.OnConnectionFailedListener {
  43. /** Listener for sign-in success or failure events. */
  44. public interface GameHelperListener {
  45. /**
  46. * Called when sign-in fails. As a result, a "Sign-In" button can be
  47. * shown to the user; when that button is clicked, call
  48. * @link{GamesHelper#beginUserInitiatedSignIn}. Note that not all calls to this
  49. * method mean an error; it may be a result of the fact that automatic
  50. * sign-in could not proceed because user interaction was required
  51. * (consent dialogs). So implementations of this method should NOT
  52. * display an error message unless a call to @link{GamesHelper#hasSignInError}
  53. * indicates that an error indeed occurred.
  54. */
  55. void onSignInFailed();
  56. /** Called when sign-in succeeds. */
  57. void onSignInSucceeded();
  58. }
  59. // States we can be in
  60. public static final int STATE_UNCONFIGURED = 0;
  61. public static final int STATE_DISCONNECTED = 1;
  62. public static final int STATE_CONNECTING = 2;
  63. public static final int STATE_CONNECTED = 3;
  64. // State names (for debug logging, etc)
  65. public static final String[] STATE_NAMES = {
  66. "UNCONFIGURED", "DISCONNECTED", "CONNECTING", "CONNECTED"
  67. };
  68. // State we are in right now
  69. int mState = STATE_UNCONFIGURED;
  70. // Are we expecting the result of a resolution flow?
  71. boolean mExpectingResolution = false;
  72. /**
  73. * The Activity we are bound to. We need to keep a reference to the Activity
  74. * because some games methods require an Activity (a Context won't do). We
  75. * are careful not to leak these references: we release them on onStop().
  76. */
  77. Activity mActivity = null;
  78. // OAuth scopes required for the clients. Initialized in setup().
  79. String mScopes[];
  80. // Request code we use when invoking other Activities to complete the
  81. // sign-in flow.
  82. final static int RC_RESOLVE = 9001;
  83. // Request code when invoking Activities whose result we don't care about.
  84. final static int RC_UNUSED = 9002;
  85. // Client objects we manage. If a given client is not enabled, it is null.
  86. GamesClient mGamesClient = null;
  87. PlusClient mPlusClient = null;
  88. AppStateClient mAppStateClient = null;
  89. // What clients we manage (OR-able values, can be combined as flags)
  90. public final static int CLIENT_NONE = 0x00;
  91. public final static int CLIENT_GAMES = 0x01;
  92. public final static int CLIENT_PLUS = 0x02;
  93. public final static int CLIENT_APPSTATE = 0x04;
  94. public final static int CLIENT_ALL = CLIENT_GAMES | CLIENT_PLUS | CLIENT_APPSTATE;
  95. // What clients were requested? (bit flags)
  96. int mRequestedClients = CLIENT_NONE;
  97. // What clients are currently connected? (bit flags)
  98. int mConnectedClients = CLIENT_NONE;
  99. // What client are we currently connecting?
  100. int mClientCurrentlyConnecting = CLIENT_NONE;
  101. // Whether to automatically try to sign in on onStart().
  102. boolean mAutoSignIn = true;
  103. /*
  104. * Whether user has specifically requested that the sign-in process begin.
  105. * If mUserInitiatedSignIn is false, we're in the automatic sign-in attempt
  106. * that we try once the Activity is started -- if true, then the user has
  107. * already clicked a "Sign-In" button or something similar
  108. */
  109. boolean mUserInitiatedSignIn = false;
  110. // The connection result we got from our last attempt to sign-in.
  111. ConnectionResult mConnectionResult = null;
  112. // The error that happened during sign-in.
  113. SignInFailureReason mSignInFailureReason = null;
  114. // Print debug logs?
  115. boolean mDebugLog = false;
  116. String mDebugTag = "GameHelper";
  117. /*
  118. * If we got an invitation id when we connected to the games client, it's
  119. * here. Otherwise, it's null.
  120. */
  121. String mInvitationId;
  122. // Listener
  123. GameHelperListener mListener = null;
  124. /**
  125. * Construct a GameHelper object, initially tied to the given Activity.
  126. * After constructing this object, call @link{setup} from the onCreate()
  127. * method of your Activity.
  128. */
  129. public GameHelper(Activity activity) {
  130. mActivity = activity;
  131. }
  132. static private final int TYPE_DEVELOPER_ERROR = 1001;
  133. static private final int TYPE_GAMEHELPER_BUG = 1002;
  134. boolean checkState(int type, String operation, String warning, int... expectedStates) {
  135. for (int expectedState : expectedStates) {
  136. if (mState == expectedState) {
  137. return true;
  138. }
  139. }
  140. StringBuilder sb = new StringBuilder();
  141. if (type == TYPE_DEVELOPER_ERROR) {
  142. sb.append("GameHelper: you attempted an operation at an invalid. ");
  143. } else {
  144. sb.append("GameHelper: bug detected. Please report it at our bug tracker ");
  145. sb.append("https://github.com/playgameservices/android-samples/issues. ");
  146. sb.append("Please include the last couple hundred lines of logcat output ");
  147. sb.append("and describe the operation that caused this. ");
  148. }
  149. sb.append("Explanation: ").append(warning);
  150. sb.append("Operation: ").append(operation).append(". ");
  151. sb.append("State: ").append(STATE_NAMES[mState]).append(". ");
  152. if (expectedStates.length == 1) {
  153. sb.append("Expected state: ").append(STATE_NAMES[expectedStates[0]]).append(".");
  154. } else {
  155. sb.append("Expected states:");
  156. for (int expectedState : expectedStates) {
  157. sb.append(" " ).append(STATE_NAMES[expectedState]);
  158. }
  159. sb.append(".");
  160. }
  161. logWarn(sb.toString());
  162. return false;
  163. }
  164. void assertConfigured(String operation) {
  165. if (mState == STATE_UNCONFIGURED) {
  166. String error = "GameHelper error: Operation attempted without setup: " + operation +
  167. ". The setup() method must be called before attempting any other operation.";
  168. logError(error);
  169. throw new IllegalStateException(error);
  170. }
  171. }
  172. /**
  173. * Same as calling @link{setup(GameHelperListener, int)} requesting only the
  174. * CLIENT_GAMES client.
  175. */
  176. public void setup(GameHelperListener listener) {
  177. setup(listener, CLIENT_GAMES);
  178. }
  179. /**
  180. * Performs setup on this GameHelper object. Call this from the onCreate()
  181. * method of your Activity. This will create the clients and do a few other
  182. * initialization tasks. Next, call @link{#onStart} from the onStart()
  183. * method of your Activity.
  184. *
  185. * @param listener The listener to be notified of sign-in events.
  186. * @param clientsToUse The clients to use. Use a combination of
  187. * CLIENT_GAMES, CLIENT_PLUS and CLIENT_APPSTATE, or CLIENT_ALL
  188. * to request all clients.
  189. * @param additionalScopes Any scopes to be used that are outside of the ones defined
  190. * in the Scopes class.
  191. * I.E. for YouTube uploads one would add
  192. * "https://www.googleapis.com/auth/youtube.upload"
  193. */
  194. public void setup(GameHelperListener listener, int clientsToUse, String ... additionalScopes) {
  195. if (mState != STATE_UNCONFIGURED) {
  196. String error = "GameHelper: you called GameHelper.setup() twice. You can only call " +
  197. "it once.";
  198. logError(error);
  199. throw new IllegalStateException(error);
  200. }
  201. mListener = listener;
  202. mRequestedClients = clientsToUse;
  203. debugLog("Setup: requested clients: " + mRequestedClients);
  204. Vector<String> scopesVector = new Vector<String>();
  205. if (0 != (clientsToUse & CLIENT_GAMES)) {
  206. scopesVector.add(Scopes.GAMES);
  207. }
  208. if (0 != (clientsToUse & CLIENT_PLUS)) {
  209. scopesVector.add(Scopes.PLUS_LOGIN);
  210. }
  211. if (0 != (clientsToUse & CLIENT_APPSTATE)) {
  212. scopesVector.add(Scopes.APP_STATE);
  213. }
  214. if (null != additionalScopes) {
  215. for (String scope : additionalScopes) {
  216. scopesVector.add(scope);
  217. }
  218. }
  219. mScopes = new String[scopesVector.size()];
  220. scopesVector.copyInto(mScopes);
  221. debugLog("setup: scopes:");
  222. for (String scope : mScopes) {
  223. debugLog(" - " + scope);
  224. }
  225. if (0 != (clientsToUse & CLIENT_GAMES)) {
  226. debugLog("setup: creating GamesClient");
  227. mGamesClient = new GamesClient.Builder(getContext(), this, this)
  228. .setGravityForPopups(Gravity.TOP | Gravity.CENTER_HORIZONTAL)
  229. .setScopes(mScopes)
  230. .create();
  231. }
  232. if (0 != (clientsToUse & CLIENT_PLUS)) {
  233. debugLog("setup: creating GamesPlusClient");
  234. mPlusClient = new PlusClient.Builder(getContext(), this, this)
  235. .setScopes(mScopes)
  236. .build();
  237. }
  238. if (0 != (clientsToUse & CLIENT_APPSTATE)) {
  239. debugLog("setup: creating AppStateClient");
  240. mAppStateClient = new AppStateClient.Builder(getContext(), this, this)
  241. .setScopes(mScopes)
  242. .create();
  243. }
  244. setState(STATE_DISCONNECTED);
  245. }
  246. void setState(int newState) {
  247. String oldStateName = STATE_NAMES[mState];
  248. String newStateName = STATE_NAMES[newState];
  249. mState = newState;
  250. debugLog("State change " + oldStateName + " -> " + newStateName);
  251. }
  252. /**
  253. * Returns the GamesClient object. In order to call this method, you must have
  254. * called @link{setup} with a set of clients that includes CLIENT_GAMES.
  255. */
  256. public GamesClient getGamesClient() {
  257. if (mGamesClient == null) {
  258. throw new IllegalStateException("No GamesClient. Did you request it at setup?");
  259. }
  260. return mGamesClient;
  261. }
  262. /**
  263. * Returns the AppStateClient object. In order to call this method, you must have
  264. * called @link{#setup} with a set of clients that includes CLIENT_APPSTATE.
  265. */
  266. public AppStateClient getAppStateClient() {
  267. if (mAppStateClient == null) {
  268. throw new IllegalStateException("No AppStateClient. Did you request it at setup?");
  269. }
  270. return mAppStateClient;
  271. }
  272. /**
  273. * Returns the PlusClient object. In order to call this method, you must have
  274. * called @link{#setup} with a set of clients that includes CLIENT_PLUS.
  275. */
  276. public PlusClient getPlusClient() {
  277. if (mPlusClient == null) {
  278. throw new IllegalStateException("No PlusClient. Did you request it at setup?");
  279. }
  280. return mPlusClient;
  281. }
  282. /** Returns whether or not the user is signed in. */
  283. public boolean isSignedIn() {
  284. return mState == STATE_CONNECTED;
  285. }
  286. /**
  287. * Returns whether or not there was a (non-recoverable) error during the
  288. * sign-in process.
  289. */
  290. public boolean hasSignInError() {
  291. return mSignInFailureReason != null;
  292. }
  293. /**
  294. * Returns the error that happened during the sign-in process, null if no
  295. * error occurred.
  296. */
  297. public SignInFailureReason getSignInError() {
  298. return mSignInFailureReason;
  299. }
  300. /** Call this method from your Activity's onStart(). */
  301. public void onStart(Activity act) {
  302. mActivity = act;
  303. debugLog("onStart, state = " + STATE_NAMES[mState]);
  304. assertConfigured("onStart");
  305. switch (mState) {
  306. case STATE_DISCONNECTED:
  307. // we are not connected, so attempt to connect
  308. if (mAutoSignIn) {
  309. debugLog("onStart: Now connecting clients.");
  310. startConnections();
  311. } else {
  312. debugLog("onStart: Not connecting (user specifically signed out).");
  313. }
  314. break;
  315. case STATE_CONNECTING:
  316. // connection process is in progress; no action required
  317. debugLog("onStart: connection process in progress, no action taken.");
  318. break;
  319. case STATE_CONNECTED:
  320. // already connected (for some strange reason). No complaints :-)
  321. debugLog("onStart: already connected (unusual, but ok).");
  322. break;
  323. default:
  324. String msg = "onStart: BUG: unexpected state " + STATE_NAMES[mState];
  325. logError(msg);
  326. throw new IllegalStateException(msg);
  327. }
  328. }
  329. /** Call this method from your Activity's onStop(). */
  330. public void onStop() {
  331. debugLog("onStop, state = " + STATE_NAMES[mState]);
  332. assertConfigured("onStop");
  333. switch (mState) {
  334. case STATE_CONNECTED:
  335. case STATE_CONNECTING:
  336. // kill connections
  337. debugLog("onStop: Killing connections");
  338. killConnections();
  339. break;
  340. case STATE_DISCONNECTED:
  341. debugLog("onStop: not connected, so no action taken.");
  342. break;
  343. default:
  344. String msg = "onStop: BUG: unexpected state " + STATE_NAMES[mState];
  345. logError(msg);
  346. throw new IllegalStateException(msg);
  347. }
  348. // let go of the Activity reference
  349. mActivity = null;
  350. }
  351. /** Convenience method to show an alert dialog. */
  352. public void showAlert(String title, String message) {
  353. (new AlertDialog.Builder(getContext())).setTitle(title).setMessage(message)
  354. .setNeutralButton(android.R.string.ok, null).create().show();
  355. }
  356. /** Convenience method to show an alert dialog. */
  357. public void showAlert(String message) {
  358. (new AlertDialog.Builder(getContext())).setMessage(message)
  359. .setNeutralButton(android.R.string.ok, null).create().show();
  360. }
  361. /**
  362. * Returns the invitation ID received through an invitation notification.
  363. * This should be called from your GameHelperListener's
  364. *
  365. * @link{GameHelperListener#onSignInSucceeded} method, to check if there's an
  366. * invitation available. In that case, accept the invitation.
  367. * @return The id of the invitation, or null if none was received.
  368. */
  369. public String getInvitationId() {
  370. if (!checkState(TYPE_DEVELOPER_ERROR, "getInvitationId",
  371. "Invitation ID is only available when connected " +
  372. "(after getting the onSignInSucceeded callback).", STATE_CONNECTED)) {
  373. return null;
  374. }
  375. return mInvitationId;
  376. }
  377. /** Enables debug logging */
  378. public void enableDebugLog(boolean enabled, String tag) {
  379. mDebugLog = enabled;
  380. mDebugTag = tag;
  381. if (enabled) {
  382. debugLog("Debug log enabled, tag: " + tag);
  383. }
  384. }
  385. /**
  386. * Returns the current requested scopes. This is not valid until setup() has
  387. * been called.
  388. *
  389. * @return the requested scopes, including the oauth2: prefix
  390. */
  391. public String getScopes() {
  392. StringBuilder scopeStringBuilder = new StringBuilder();
  393. if (null != mScopes) {
  394. for (String scope: mScopes) {
  395. addToScope(scopeStringBuilder, scope);
  396. }
  397. }
  398. return scopeStringBuilder.toString();
  399. }
  400. /**
  401. * Returns an array of the current requested scopes. This is not valid until
  402. * setup() has been called
  403. *
  404. * @return the requested scopes, including the oauth2: prefix
  405. */
  406. public String[] getScopesArray() {
  407. return mScopes;
  408. }
  409. /** Sign out and disconnect from the APIs. */
  410. public void signOut() {
  411. if (mState == STATE_DISCONNECTED) {
  412. // nothing to do
  413. debugLog("signOut: state was already DISCONNECTED, ignoring.");
  414. return;
  415. }
  416. // for the PlusClient, "signing out" means clearing the default account and
  417. // then disconnecting
  418. if (mPlusClient != null && mPlusClient.isConnected()) {
  419. debugLog("Clearing default account on PlusClient.");
  420. mPlusClient.clearDefaultAccount();
  421. }
  422. // For the games client, signing out means calling signOut and disconnecting
  423. if (mGamesClient != null && mGamesClient.isConnected()) {
  424. debugLog("Signing out from GamesClient.");
  425. mGamesClient.signOut();
  426. }
  427. // Ready to disconnect
  428. debugLog("Proceeding with disconnection.");
  429. killConnections();
  430. }
  431. void killConnections() {
  432. if (!checkState(TYPE_GAMEHELPER_BUG, "killConnections", "killConnections() should only " +
  433. "get called while connected or connecting.", STATE_CONNECTED, STATE_CONNECTING)) {
  434. return;
  435. }
  436. debugLog("killConnections: killing connections.");
  437. mConnectionResult = null;
  438. mSignInFailureReason = null;
  439. if (mGamesClient != null && mGamesClient.isConnected()) {
  440. debugLog("Disconnecting GamesClient.");
  441. mGamesClient.disconnect();
  442. }
  443. if (mPlusClient != null && mPlusClient.isConnected()) {
  444. debugLog("Disconnecting PlusClient.");
  445. mPlusClient.disconnect();
  446. }
  447. if (mAppStateClient != null && mAppStateClient.isConnected()) {
  448. debugLog("Disconnecting AppStateClient.");
  449. mAppStateClient.disconnect();
  450. }
  451. mConnectedClients = CLIENT_NONE;
  452. debugLog("killConnections: all clients disconnected.");
  453. setState(STATE_DISCONNECTED);
  454. }
  455. static String activityResponseCodeToString(int respCode) {
  456. switch (respCode) {
  457. case Activity.RESULT_OK:
  458. return "RESULT_OK";
  459. case Activity.RESULT_CANCELED:
  460. return "RESULT_CANCELED";
  461. case GamesActivityResultCodes.RESULT_APP_MISCONFIGURED:
  462. return "RESULT_APP_MISCONFIGURED";
  463. case GamesActivityResultCodes.RESULT_LEFT_ROOM:
  464. return "RESULT_LEFT_ROOM";
  465. case GamesActivityResultCodes.RESULT_LICENSE_FAILED:
  466. return "RESULT_LICENSE_FAILED";
  467. case GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED:
  468. return "RESULT_RECONNECT_REQUIRED";
  469. case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED:
  470. return "SIGN_IN_FAILED";
  471. default:
  472. return String.valueOf(respCode);
  473. }
  474. }
  475. /**
  476. * Handle activity result. Call this method from your Activity's
  477. * onActivityResult callback. If the activity result pertains to the sign-in
  478. * process, processes it appropriately.
  479. */
  480. public void onActivityResult(int requestCode, int responseCode, Intent intent) {
  481. debugLog("onActivityResult: req=" + (requestCode == RC_RESOLVE ? "RC_RESOLVE" :
  482. String.valueOf(requestCode)) + ", resp=" +
  483. activityResponseCodeToString(responseCode));
  484. if (requestCode != RC_RESOLVE) {
  485. debugLog("onActivityResult: request code not meant for us. Ignoring.");
  486. return;
  487. }
  488. // no longer expecting a resolution
  489. mExpectingResolution = false;
  490. if (mState != STATE_CONNECTING) {
  491. debugLog("onActivityResult: ignoring because state isn't STATE_CONNECTING (" +
  492. "it's " + STATE_NAMES[mState] + ")");
  493. return;
  494. }
  495. // We're coming back from an activity that was launched to resolve a
  496. // connection problem. For example, the sign-in UI.
  497. if (responseCode == Activity.RESULT_OK) {
  498. // Ready to try to connect again.
  499. debugLog("onAR: Resolution was RESULT_OK, so connecting current client again.");
  500. connectCurrentClient();
  501. } else if (responseCode == GamesActivityResultCodes.RESULT_RECONNECT_REQUIRED) {
  502. debugLog("onAR: Resolution was RECONNECT_REQUIRED, so reconnecting.");
  503. connectCurrentClient();
  504. } else if (responseCode == Activity.RESULT_CANCELED) {
  505. // User cancelled.
  506. debugLog("onAR: Got a cancellation result, so disconnecting.");
  507. mAutoSignIn = false;
  508. mUserInitiatedSignIn = false;
  509. mSignInFailureReason = null; // cancelling is not a failure!
  510. killConnections();
  511. notifyListener(false);
  512. } else {
  513. // Whatever the problem we were trying to solve, it was not
  514. // solved. So give up and show an error message.
  515. debugLog("onAR: responseCode=" + activityResponseCodeToString(responseCode) +
  516. ", so giving up.");
  517. giveUp(new SignInFailureReason(mConnectionResult.getErrorCode(), responseCode));
  518. }
  519. }
  520. void notifyListener(boolean success) {
  521. debugLog("Notifying LISTENER of sign-in " + (success ? "SUCCESS" :
  522. mSignInFailureReason != null ? "FAILURE (error)" : "FAILURE (no error)"));
  523. if (mListener != null) {
  524. if (success) {
  525. mListener.onSignInSucceeded();
  526. } else {
  527. mListener.onSignInFailed();
  528. }
  529. }
  530. }
  531. /**
  532. * Starts a user-initiated sign-in flow. This should be called when the user
  533. * clicks on a "Sign In" button. As a result, authentication/consent dialogs
  534. * may show up. At the end of the process, the GameHelperListener's
  535. * onSignInSucceeded() or onSignInFailed() methods will be called.
  536. */
  537. public void beginUserInitiatedSignIn() {
  538. if (mState == STATE_CONNECTED) {
  539. // nothing to do
  540. logWarn("beginUserInitiatedSignIn() called when already connected. " +
  541. "Calling listener directly to notify of success.");
  542. notifyListener(true);
  543. return;
  544. } else if (mState == STATE_CONNECTING) {
  545. logWarn("beginUserInitiatedSignIn() called when already connecting. " +
  546. "Be patient! You can only call this method after you get an " +
  547. "onSignInSucceeded() or onSignInFailed() callback. Suggestion: disable " +
  548. "the sign-in button on startup and also when it's clicked, and re-enable " +
  549. "when you get the callback.");
  550. // ignore call (listener will get a callback when the connection process finishes)
  551. return;
  552. }
  553. debugLog("Starting USER-INITIATED sign-in flow.");
  554. // sign in automatically on onStart()
  555. mAutoSignIn = true;
  556. // Is Google Play services available?
  557. int result = GooglePlayServicesUtil.isGooglePlayServicesAvailable(getContext());
  558. debugLog("isGooglePlayServicesAvailable returned " + result);
  559. if (result != ConnectionResult.SUCCESS) {
  560. // Google Play services is not available.
  561. debugLog("Google Play services not available. Show error dialog.");
  562. mSignInFailureReason = new SignInFailureReason(result, 0);
  563. showFailureDialog();
  564. notifyListener(false);
  565. return;
  566. }
  567. // indicate that user is actively trying to sign in (so we know to resolve
  568. // connection problems by showing dialogs)
  569. mUserInitiatedSignIn = true;
  570. if (mConnectionResult != null) {
  571. // We have a pending connection result from a previous failure, so
  572. // start with that.
  573. debugLog("beginUserInitiatedSignIn: continuing pending sign-in flow.");
  574. setState(STATE_CONNECTING);
  575. resolveConnectionResult();
  576. } else {
  577. // We don't have a pending connection result, so start anew.
  578. debugLog("beginUserInitiatedSignIn: starting new sign-in flow.");
  579. startConnections();
  580. }
  581. }
  582. Context getContext() {
  583. return mActivity;
  584. }
  585. void addToScope(StringBuilder scopeStringBuilder, String scope) {
  586. if (scopeStringBuilder.length() == 0) {
  587. scopeStringBuilder.append("oauth2:");
  588. } else {
  589. scopeStringBuilder.append(" ");
  590. }
  591. scopeStringBuilder.append(scope);
  592. }
  593. void startConnections() {
  594. if (!checkState(TYPE_GAMEHELPER_BUG, "startConnections", "startConnections should " +
  595. "only get called when disconnected.", STATE_DISCONNECTED)) {
  596. return;
  597. }
  598. debugLog("Starting connections.");
  599. setState(STATE_CONNECTING);
  600. mInvitationId = null;
  601. connectNextClient();
  602. }
  603. void connectNextClient() {
  604. // do we already have all the clients we need?
  605. debugLog("connectNextClient: requested clients: " + mRequestedClients +
  606. ", connected clients: " + mConnectedClients);
  607. // failsafe, in case we somehow lost track of what clients are connected or not.
  608. if (mGamesClient != null && mGamesClient.isConnected() &&
  609. (0 == (mConnectedClients & CLIENT_GAMES))) {
  610. logWarn("GamesClient was already connected. Fixing.");
  611. mConnectedClients |= CLIENT_GAMES;
  612. }
  613. if (mPlusClient != null && mPlusClient.isConnected() &&
  614. (0 == (mConnectedClients & CLIENT_PLUS))) {
  615. logWarn("PlusClient was already connected. Fixing.");
  616. mConnectedClients |= CLIENT_PLUS;
  617. }
  618. if (mAppStateClient != null && mAppStateClient.isConnected() &&
  619. (0 == (mConnectedClients & CLIENT_APPSTATE))) {
  620. logWarn("AppStateClient was already connected. Fixing");
  621. mConnectedClients |= CLIENT_APPSTATE;
  622. }
  623. int pendingClients = mRequestedClients & ~mConnectedClients;
  624. debugLog("Pending clients: " + pendingClients);
  625. if (pendingClients == 0) {
  626. debugLog("All clients now connected. Sign-in successful!");
  627. succeedSignIn();
  628. return;
  629. }
  630. // which client should be the next one to connect?
  631. if (mGamesClient != null && (0 != (pendingClients & CLIENT_GAMES))) {
  632. debugLog("Connecting GamesClient.");
  633. mClientCurrentlyConnecting = CLIENT_GAMES;
  634. } else if (mPlusClient != null && (0 != (pendingClients & CLIENT_PLUS))) {
  635. debugLog("Connecting PlusClient.");
  636. mClientCurrentlyConnecting = CLIENT_PLUS;
  637. } else if (mAppStateClient != null && (0 != (pendingClients & CLIENT_APPSTATE))) {
  638. debugLog("Connecting AppStateClient.");
  639. mClientCurrentlyConnecting = CLIENT_APPSTATE;
  640. } else {
  641. // hmmm, getting here would be a bug.
  642. throw new AssertionError("Not all clients connected, yet no one is next. R="
  643. + mRequestedClients + ", C=" + mConnectedClients);
  644. }
  645. connectCurrentClient();
  646. }
  647. void connectCurrentClient() {
  648. if (mState == STATE_DISCONNECTED) {
  649. // we got disconnected during the connection process, so abort
  650. logWarn("GameHelper got disconnected during connection process. Aborting.");
  651. return;
  652. }
  653. if (!checkState(TYPE_GAMEHELPER_BUG, "connectCurrentClient", "connectCurrentClient " +
  654. "should only get called when connecting.", STATE_CONNECTING)) {
  655. return;
  656. }
  657. switch (mClientCurrentlyConnecting) {
  658. case CLIENT_GAMES:
  659. mGamesClient.connect();
  660. break;
  661. case CLIENT_APPSTATE:
  662. mAppStateClient.connect();
  663. break;
  664. case CLIENT_PLUS:
  665. mPlusClient.connect();
  666. break;
  667. }
  668. }
  669. /**
  670. * Disconnects the indicated clients, then connects them again.
  671. * @param whatClients Indicates which clients to reconnect.
  672. */
  673. public void reconnectClients(int whatClients) {
  674. checkState(TYPE_DEVELOPER_ERROR, "reconnectClients", "reconnectClients should " +
  675. "only be called when connected. Proceeding anyway.", STATE_CONNECTED);
  676. boolean actuallyReconnecting = false;
  677. if ((whatClients & CLIENT_GAMES) != 0 && mGamesClient != null
  678. && mGamesClient.isConnected()) {
  679. debugLog("Reconnecting GamesClient.");
  680. actuallyReconnecting = true;
  681. mConnectedClients &= ~CLIENT_GAMES;
  682. mGamesClient.reconnect();
  683. }
  684. if ((whatClients & CLIENT_APPSTATE) != 0 && mAppStateClient != null
  685. && mAppStateClient.isConnected()) {
  686. debugLog("Reconnecting AppStateClient.");
  687. actuallyReconnecting = true;
  688. mConnectedClients &= ~CLIENT_APPSTATE;
  689. mAppStateClient.reconnect();
  690. }
  691. if ((whatClients & CLIENT_PLUS) != 0 && mPlusClient != null
  692. && mPlusClient.isConnected()) {
  693. // PlusClient doesn't need reconnections.
  694. logWarn("GameHelper is ignoring your request to reconnect " +
  695. "PlusClient because this is unnecessary.");
  696. }
  697. if (actuallyReconnecting) {
  698. setState(STATE_CONNECTING);
  699. } else {
  700. // No reconnections are to take place, so for consistency we call the listener
  701. // as if sign in had just succeeded.
  702. debugLog("No reconnections needed, so behaving as if sign in just succeeded");
  703. notifyListener(true);
  704. }
  705. }
  706. /** Called when we successfully obtain a connection to a client. */
  707. @Override
  708. public void onConnected(Bundle connectionHint) {
  709. debugLog("onConnected: connected! client=" + mClientCurrentlyConnecting);
  710. // Mark the current client as connected
  711. mConnectedClients |= mClientCurrentlyConnecting;
  712. debugLog("Connected clients updated to: " + mConnectedClients);
  713. // If this was the games client and it came with an invite, store it for
  714. // later retrieval.
  715. if (mClientCurrentlyConnecting == CLIENT_GAMES && connectionHint != null) {
  716. debugLog("onConnected: connection hint provided. Checking for invite.");
  717. Invitation inv = connectionHint.getParcelable(GamesClient.EXTRA_INVITATION);
  718. if (inv != null && inv.getInvitationId() != null) {
  719. // accept invitation
  720. debugLog("onConnected: connection hint has a room invite!");
  721. mInvitationId = inv.getInvitationId();
  722. debugLog("Invitation ID: " + mInvitationId);
  723. }
  724. }
  725. // connect the next client in line, if any.
  726. connectNextClient();
  727. }
  728. void succeedSignIn() {
  729. checkState(TYPE_GAMEHELPER_BUG, "succeedSignIn", "succeedSignIn should only " +
  730. "get called in the connecting or connected state. Proceeding anyway.",
  731. STATE_CONNECTING, STATE_CONNECTED);
  732. debugLog("All requested clients connected. Sign-in succeeded!");
  733. setState(STATE_CONNECTED);
  734. mSignInFailureReason = null;
  735. mAutoSignIn = true;
  736. mUserInitiatedSignIn = false;
  737. notifyListener(true);
  738. }
  739. /** Handles a connection failure reported by a client. */
  740. @Override
  741. public void onConnectionFailed(ConnectionResult result) {
  742. // save connection result for later reference
  743. debugLog("onConnectionFailed");
  744. mConnectionResult = result;
  745. debugLog("Connection failure:");
  746. debugLog(" - code: " + errorCodeToString(mConnectionResult.getErrorCode()));
  747. debugLog(" - resolvable: " + mConnectionResult.hasResolution());
  748. debugLog(" - details: " + mConnectionResult.toString());
  749. if (!mUserInitiatedSignIn) {
  750. // If the user didn't initiate the sign-in, we don't try to resolve
  751. // the connection problem automatically -- instead, we fail and wait
  752. // for the user to want to sign in. That way, they won't get an
  753. // authentication (or other) popup unless they are actively trying
  754. // to
  755. // sign in.
  756. debugLog("onConnectionFailed: since user didn't initiate sign-in, failing now.");
  757. mConnectionResult = result;
  758. setState(STATE_DISCONNECTED);
  759. notifyListener(false);
  760. return;
  761. }
  762. debugLog("onConnectionFailed: since user initiated sign-in, resolving problem.");
  763. // Resolve the connection result. This usually means showing a dialog or
  764. // starting an Activity that will allow the user to give the appropriate
  765. // consents so that sign-in can be successful.
  766. resolveConnectionResult();
  767. }
  768. /**
  769. * Attempts to resolve a connection failure. This will usually involve
  770. * starting a UI flow that lets the user give the appropriate consents
  771. * necessary for sign-in to work.
  772. */
  773. void resolveConnectionResult() {
  774. // Try to resolve the problem
  775. checkState(TYPE_GAMEHELPER_BUG, "resolveConnectionResult",
  776. "resolveConnectionResult should only be called when connecting. Proceeding anyway.",
  777. STATE_CONNECTING);
  778. if (mExpectingResolution) {
  779. debugLog("We're already expecting the result of a previous resolution.");
  780. return;
  781. }
  782. debugLog("resolveConnectionResult: trying to resolve result: " + mConnectionResult);
  783. if (mConnectionResult.hasResolution()) {
  784. // This problem can be fixed. So let's try to fix it.
  785. debugLog("Result has resolution. Starting it.");
  786. try {
  787. // launch appropriate UI flow (which might, for example, be the
  788. // sign-in flow)
  789. mExpectingResolution = true;
  790. mConnectionResult.startResolutionForResult(mActivity, RC_RESOLVE);
  791. } catch (SendIntentException e) {
  792. // Try connecting again
  793. debugLog("SendIntentException, so connecting again.");
  794. connectCurrentClient();
  795. }
  796. } else {
  797. // It's not a problem what we can solve, so give up and show an
  798. // error.
  799. debugLog("resolveConnectionResult: result has no resolution. Giving up.");
  800. giveUp(new SignInFailureReason(mConnectionResult.getErrorCode()));
  801. }
  802. }
  803. /**
  804. * Give up on signing in due to an error. Shows the appropriate error
  805. * message to the user, using a standard error dialog as appropriate to the
  806. * cause of the error. That dialog will indicate to the user how the problem
  807. * can be solved (for example, re-enable Google Play Services, upgrade to a
  808. * new version, etc).
  809. */
  810. void giveUp(SignInFailureReason reason) {
  811. checkState(TYPE_GAMEHELPER_BUG, "giveUp", "giveUp should only be called when " +
  812. "connecting. Proceeding anyway.", STATE_CONNECTING);
  813. mAutoSignIn = false;
  814. killConnections();
  815. mSignInFailureReason = reason;
  816. showFailureDialog();
  817. notifyListener(false);
  818. }
  819. /** Called when we are disconnected from a client. */
  820. @Override
  821. public void onDisconnected() {
  822. debugLog("onDisconnected.");
  823. if (mState == STATE_DISCONNECTED) {
  824. // This is expected.
  825. debugLog("onDisconnected is expected, so no action taken.");
  826. return;
  827. }
  828. // Unexpected disconnect (rare!)
  829. logWarn("Unexpectedly disconnected. Severing remaining connections.");
  830. // kill the other connections too, and revert to DISCONNECTED state.
  831. killConnections();
  832. mSignInFailureReason = null;
  833. // call the sign in failure callback
  834. debugLog("Making extraordinary call to onSignInFailed callback");
  835. notifyListener(false);
  836. }
  837. /** Shows an error dialog that's appropriate for the failure reason. */
  838. void showFailureDialog() {
  839. Context ctx = getContext();
  840. if (ctx == null) {
  841. debugLog("*** No context. Can't show failure dialog.");
  842. return;
  843. }
  844. debugLog("Making error dialog for failure: " + mSignInFailureReason);
  845. Dialog errorDialog = null;
  846. int errorCode = mSignInFailureReason.getServiceErrorCode();
  847. int actResp = mSignInFailureReason.getActivityResultCode();
  848. switch (actResp) {
  849. case GamesActivityResultCodes.RESULT_APP_MISCONFIGURED:
  850. errorDialog = makeSimpleDialog(ctx.getString(
  851. R.string.gamehelper_app_misconfigured));
  852. printMisconfiguredDebugInfo();
  853. break;
  854. case GamesActivityResultCodes.RESULT_SIGN_IN_FAILED:
  855. errorDialog = makeSimpleDialog(ctx.getString(
  856. R.string.gamehelper_sign_in_failed));
  857. break;
  858. case GamesActivityResultCodes.RESULT_LICENSE_FAILED:
  859. errorDialog = makeSimpleDialog(ctx.getString(
  860. R.string.gamehelper_license_failed));
  861. break;
  862. default:
  863. // No meaningful Activity response code, so generate default Google
  864. // Play services dialog
  865. errorDialog = GooglePlayServicesUtil.getErrorDialog(errorCode, mActivity,
  866. RC_UNUSED, null);
  867. if (errorDialog == null) {
  868. // get fallback dialog
  869. debugLog("No standard error dialog available. Making fallback dialog.");
  870. errorDialog = makeSimpleDialog(ctx.getString(R.string.gamehelper_unknown_error)
  871. + " " + errorCodeToString(errorCode));
  872. }
  873. }
  874. debugLog("Showing error dialog.");
  875. errorDialog.show();
  876. }
  877. Dialog makeSimpleDialog(String text) {
  878. return (new AlertDialog.Builder(getContext())).setMessage(text)
  879. .setNeutralButton(android.R.string.ok, null).create();
  880. }
  881. void debugLog(String message) {
  882. if (mDebugLog) {
  883. Log.d(mDebugTag, "GameHelper: " + message);
  884. }
  885. }
  886. void logWarn(String message) {
  887. Log.w(mDebugTag, "!!! GameHelper WARNING: " + message);
  888. }
  889. void logError(String message) {
  890. Log.e(mDebugTag, "*** GameHelper ERROR: " + message);
  891. }
  892. static String errorCodeToString(int errorCode) {
  893. switch (errorCode) {
  894. case ConnectionResult.DEVELOPER_ERROR:
  895. return "DEVELOPER_ERROR(" + errorCode + ")";
  896. case ConnectionResult.INTERNAL_ERROR:
  897. return "INTERNAL_ERROR(" + errorCode + ")";
  898. case ConnectionResult.INVALID_ACCOUNT:
  899. return "INVALID_ACCOUNT(" + errorCode + ")";
  900. case ConnectionResult.LICENSE_CHECK_FAILED:
  901. return "LICENSE_CHECK_FAILED(" + errorCode + ")";
  902. case ConnectionResult.NETWORK_ERROR:
  903. return "NETWORK_ERROR(" + errorCode + ")";
  904. case ConnectionResult.RESOLUTION_REQUIRED:
  905. return "RESOLUTION_REQUIRED(" + errorCode + ")";
  906. case ConnectionResult.SERVICE_DISABLED:
  907. return "SERVICE_DISABLED(" + errorCode + ")";
  908. case ConnectionResult.SERVICE_INVALID:
  909. return "SERVICE_INVALID(" + errorCode + ")";
  910. case ConnectionResult.SERVICE_MISSING:
  911. return "SERVICE_MISSING(" + errorCode + ")";
  912. case ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
  913. return "SERVICE_VERSION_UPDATE_REQUIRED(" + errorCode + ")";
  914. case ConnectionResult.SIGN_IN_REQUIRED:
  915. return "SIGN_IN_REQUIRED(" + errorCode + ")";
  916. case ConnectionResult.SUCCESS:
  917. return "SUCCESS(" + errorCode + ")";
  918. default:
  919. return "Unknown error code " + errorCode;
  920. }
  921. }
  922. // Represents the reason for a sign-in failure
  923. public static class SignInFailureReason {
  924. public static final int NO_ACTIVITY_RESULT_CODE = -100;
  925. int mServiceErrorCode = 0;
  926. int mActivityResultCode = NO_ACTIVITY_RESULT_CODE;
  927. public int getServiceErrorCode() {
  928. return mServiceErrorCode;
  929. }
  930. public int getActivityResultCode() {
  931. return mActivityResultCode;
  932. }
  933. public SignInFailureReason(int serviceErrorCode, int activityResultCode) {
  934. mServiceErrorCode = serviceErrorCode;
  935. mActivityResultCode = activityResultCode;
  936. }
  937. public SignInFailureReason(int serviceErrorCode) {
  938. this(serviceErrorCode, NO_ACTIVITY_RESULT_CODE);
  939. }
  940. @Override
  941. public String toString() {
  942. return "SignInFailureReason(serviceErrorCode:" +
  943. errorCodeToString(mServiceErrorCode) +
  944. ((mActivityResultCode == NO_ACTIVITY_RESULT_CODE) ? ")" :
  945. (",activityResultCode:" +
  946. activityResponseCodeToString(mActivityResultCode) + ")"));
  947. }
  948. }
  949. void printMisconfiguredDebugInfo() {
  950. debugLog("****");
  951. debugLog("****");
  952. debugLog("**** APP NOT CORRECTLY CONFIGURED TO USE GOOGLE PLAY GAME SERVICES");
  953. debugLog("**** This is usually caused by one of these reasons:");
  954. debugLog("**** (1) Your package name and certificate fingerprint do not match");
  955. debugLog("**** the client ID you registered in Developer Console.");
  956. debugLog("**** (2) Your App ID was incorrectly entered.");
  957. debugLog("**** (3) Your game settings have not been published and you are ");
  958. debugLog("**** trying to log in with an account that is not listed as");
  959. debugLog("**** a test account.");
  960. debugLog("****");
  961. Context ctx = getContext();
  962. if (ctx == null) {
  963. debugLog("*** (no Context, so can't print more debug info)");
  964. return;
  965. }
  966. debugLog("**** To help you debug, here is the information about this app");
  967. debugLog("**** Package name : " + getContext().getPackageName());
  968. debugLog("**** Cert SHA1 fingerprint: " + getSHA1CertFingerprint());
  969. debugLog("**** App ID from : " + getAppIdFromResource());
  970. debugLog("****");
  971. debugLog("**** Check that the above information matches your setup in ");
  972. debugLog("**** Developer Console. Also, check that you're logging in with the");
  973. debugLog("**** right account (it should be listed in the Testers section if");
  974. debugLog("**** your project is not yet published).");
  975. debugLog("****");
  976. debugLog("**** For more information, refer to the troubleshooting guide:");
  977. debugLog("**** http://developers.google.com/games/services/android/troubleshooting");
  978. }
  979. String getAppIdFromResource() {
  980. try {
  981. Resources res = getContext().getResources();
  982. String pkgName = getContext().getPackageName();
  983. int res_id = res.getIdentifier("app_id", "string", pkgName);
  984. return res.getString(res_id);
  985. } catch (Exception ex) {
  986. ex.printStackTrace();
  987. return "??? (failed to retrieve APP ID)";
  988. }
  989. }
  990. String getSHA1CertFingerprint() {
  991. try {
  992. Signature[] sigs = getContext().getPackageManager().getPackageInfo(
  993. getContext().getPackageName(), PackageManager.GET_SIGNATURES).signatures;
  994. if (sigs.length == 0) {
  995. return "ERROR: NO SIGNATURE.";
  996. } else if (sigs.length > 1) {
  997. return "ERROR: MULTIPLE SIGNATURES";
  998. }
  999. byte[] digest = MessageDigest.getInstance("SHA1").digest(sigs[0].toByteArray());
  1000. StringBuilder hexString = new StringBuilder();
  1001. for (int i = 0; i < digest.length; ++i) {
  1002. if (i > 0) {
  1003. hexString.append(":");
  1004. }
  1005. byteToString(hexString, digest[i]);
  1006. }
  1007. return hexString.toString();
  1008. } catch (PackageManager.NameNotFoundException ex) {
  1009. ex.printStackTrace();
  1010. return "(ERROR: package not found)";
  1011. } catch (NoSuchAlgorithmException ex) {
  1012. ex.printStackTrace();
  1013. return "(ERROR: SHA1 algorithm not found)";
  1014. }
  1015. }
  1016. void byteToString(StringBuilder sb, byte b) {
  1017. int unsigned_byte = b < 0 ? b + 256 : b;
  1018. int hi = unsigned_byte / 16;
  1019. int lo = unsigned_byte % 16;
  1020. sb.append("0123456789ABCDEF".substring(hi, hi + 1));
  1021. sb.append("0123456789ABCDEF".substring(lo, lo + 1));
  1022. }
  1023. }