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