Update GoogleSignInPlugin to use and expose a Delegate (#159)
This enables other Android plugins to wrap Google sign-in
functionality. The immediate use case for this is the internal
Google SSO-auth plugin, where on Android, it uses the same
client libraries at third-party apps and shares a bunch of
code with third-party sign-in.
diff --git a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
index be8655d..ec1c6e0 100755
--- a/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
+++ b/packages/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java
@@ -29,7 +29,6 @@
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.common.api.Status;
import com.google.common.base.Joiner;
-import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
@@ -37,35 +36,18 @@
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;
import java.util.HashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
-import java.util.Queue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/** Google sign-in plugin for Flutter. */
-public class GoogleSignInPlugin
- implements MethodCallHandler,
- PluginRegistry.ActivityResultListener,
- GoogleApiClient.ConnectionCallbacks,
- GoogleApiClient.OnConnectionFailedListener {
-
- private static final int REQUEST_CODE = 53293;
- private static final int REQUEST_CODE_RESOLVE_ERROR = 1001;
-
+public class GoogleSignInPlugin implements MethodCallHandler {
private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in";
private static final String TAG = "flutter";
- private static final String ERROR_REASON_EXCEPTION = "exception";
- private static final String ERROR_REASON_STATUS = "status";
- private static final String ERROR_REASON_OPERATION_IN_PROGRESS = "operation_in_progress";
- private static final String ERROR_REASON_CONNECTION_FAILED = "connection_failed";
-
- private static final String STATE_RESOLVING_ERROR = "resolving_error";
-
private static final String METHOD_INIT = "init";
private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently";
private static final String METHOD_SIGN_IN = "signIn";
@@ -73,43 +55,16 @@
private static final String METHOD_SIGN_OUT = "signOut";
private static final String METHOD_DISCONNECT = "disconnect";
- private static final class PendingOperation {
-
- final String method;
- final Queue<Result> resultQueue = new LinkedList<>();
-
- PendingOperation(String method, Result result) {
- this.method = Preconditions.checkNotNull(method);
- resultQueue.add(Preconditions.checkNotNull(result));
- }
- }
-
- private final Activity activity;
- private final BackgroundTaskRunner backgroundTaskRunner;
- private final int requestCode;
-
- private boolean resolvingError = false; // Whether we are currently resolving a sign-in error
- private GoogleApiClient googleApiClient;
- private List<String> requestedScopes;
- private PendingOperation pendingOperation;
+ private final Delegate delegate;
public static void registerWith(PluginRegistry.Registrar registrar) {
- Activity activity = registrar.activity();
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
- final GoogleSignInPlugin instance =
- new GoogleSignInPlugin(activity, new BackgroundTaskRunner(1), REQUEST_CODE);
- registrar.addActivityResultListener(instance);
+ final GoogleSignInPlugin instance = new GoogleSignInPlugin(registrar);
channel.setMethodCallHandler(instance);
}
- private GoogleSignInPlugin(
- Activity activity, BackgroundTaskRunner backgroundTaskRunner, int requestCode) {
- this.activity = activity;
- this.backgroundTaskRunner = backgroundTaskRunner;
- this.requestCode = requestCode;
- activity
- .getApplication()
- .registerActivityLifecycleCallbacks(new GoogleApiClientConnectionManager());
+ private GoogleSignInPlugin(PluginRegistry.Registrar registrar) {
+ delegate = new Delegate(registrar);
}
@Override
@@ -118,28 +73,28 @@
case METHOD_INIT:
List<String> requestedScopes = call.argument("scopes");
String hostedDomain = call.argument("hostedDomain");
- init(result, requestedScopes, hostedDomain);
+ delegate.init(result, requestedScopes, hostedDomain);
break;
case METHOD_SIGN_IN_SILENTLY:
- signInSilently(result);
+ delegate.signInSilently(result);
break;
case METHOD_SIGN_IN:
- signIn(result);
+ delegate.signIn(result);
break;
case METHOD_GET_TOKENS:
String email = call.argument("email");
- getTokens(result, email);
+ delegate.getTokens(result, email);
break;
case METHOD_SIGN_OUT:
- signOut(result);
+ delegate.signOut(result);
break;
case METHOD_DISCONNECT:
- disconnect(result);
+ delegate.disconnect(result);
break;
default:
@@ -148,372 +103,375 @@
}
/**
- * Initializes this listener so that it is ready to perform other operations. The Dart code
- * guarantees that this will be called and completed before any other methods are invoked.
- */
- private void init(Result result, List<String> requestedScopes, String hostedDomain) {
- try {
- if (googleApiClient != null) {
- // This can happen if the scopes change, or a full restart hot reload
- googleApiClient = null;
- }
- GoogleSignInOptions.Builder optionsBuilder =
- new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail();
- // Only requests a clientId if google-services.json was present and parsed
- // by the google-services Gradle script.
- // TODO(jackson): Perhaps we should provide a mechanism to override this
- // behavior.
- int clientIdIdentifier =
- activity
- .getResources()
- .getIdentifier("default_web_client_id", "string", activity.getPackageName());
- if (clientIdIdentifier != 0) {
- optionsBuilder.requestIdToken(activity.getString(clientIdIdentifier));
- }
- for (String scope : requestedScopes) {
- optionsBuilder.requestScopes(new Scope(scope));
- }
- if (!Strings.isNullOrEmpty(hostedDomain)) {
- optionsBuilder.setHostedDomain(hostedDomain);
- }
-
- this.requestedScopes = requestedScopes;
- this.googleApiClient =
- new GoogleApiClient.Builder(activity)
- .addApi(Auth.GOOGLE_SIGN_IN_API, optionsBuilder.build())
- .addConnectionCallbacks(this)
- .addOnConnectionFailedListener(this)
- .build();
- this.googleApiClient.connect();
- } catch (Exception e) {
- Log.e(TAG, "Initialization error", e);
- result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
- }
-
- // We're not initialized until we receive `onConnected`.
- // If initialization fails, we'll receive `onConnectionFailed`
- pendingOperation = new PendingOperation(METHOD_INIT, result);
- }
-
- /**
- * Handles the case of a concurrent operation already in progress.
+ * Delegate class that does the work for the Google sign-in plugin. This is exposed as a dedicated
+ * class for use in other plugins that wrap basic sign-in functionality.
*
- * <p>Only one type of operation is allowed to be executed at a time, so if there's a pending
- * operation for a method type other than the current invocation, this will report failure on the
- * specified result object. Alternatively, if there's a pending operation for the same method
- * type, this will signal that the method is already being handled and add the specified result to
- * the pending operation's result queue.
- *
- * <p>If there's no pending operation, this method will set the pending operation to the current
- * invocation.
- *
- * @param currentMethod The current invocation.
- * @param result receives the result of the current invocation.
- * @return true iff an operation is already in progress (and thus the response is already being
- * handled).
+ * <p>All methods in this class assume that they are run to completion before any other method is
+ * invoked. In this context, "run to completion" means that their {@link Result} argument has been
+ * completed (either successfully or in error). This class provides no synchronization consructs
+ * to guarantee such behavior; callers are responsible for providing such guarantees.
*/
- private boolean checkAndSetPendingOperation(String currentMethod, Result result) {
- if (pendingOperation == null) {
- pendingOperation = new PendingOperation(currentMethod, result);
- return false;
+ public static final class Delegate {
+ private static final int REQUEST_CODE = 53293;
+ private static final int REQUEST_CODE_RESOLVE_ERROR = 1001;
+
+ private static final String ERROR_REASON_EXCEPTION = "exception";
+ private static final String ERROR_REASON_STATUS = "status";
+ private static final String ERROR_REASON_OPERATION_IN_PROGRESS = "operation_in_progress";
+ private static final String ERROR_REASON_CONNECTION_FAILED = "connection_failed";
+
+ private static final String STATE_RESOLVING_ERROR = "resolving_error";
+
+ private final Activity activity;
+ private final Handler handler = new Handler();
+ private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1);
+
+ private boolean resolvingError = false; // Whether we are currently resolving a sign-in error
+ private GoogleApiClient googleApiClient;
+ private List<String> requestedScopes;
+ private PendingOperation pendingOperation;
+
+ public Delegate(PluginRegistry.Registrar registrar) {
+ activity = registrar.activity();
+ activity.getApplication().registerActivityLifecycleCallbacks(handler);
+ registrar.addActivityResultListener(handler);
}
- if (pendingOperation.method.equals(currentMethod)) {
- // This method is already being handled
- pendingOperation.resultQueue.add(result);
- } else {
- // Only one type of operation can be in progress at a time
- result.error(ERROR_REASON_OPERATION_IN_PROGRESS, pendingOperation.method, null);
+ private void checkAndSetPendingOperation(String method, Result result) {
+ if (pendingOperation != null) {
+ throw new IllegalStateException(
+ "Concurrent operations detected: " + pendingOperation.method + ", " + method);
+ }
+ pendingOperation = new PendingOperation(method, result);
}
- return true;
- }
+ /**
+ * Initializes this delegate so that it is ready to perform other operations. The Dart code
+ * guarantees that this will be called and completed before any other methods are invoked.
+ */
+ public void init(Result result, List<String> requestedScopes, String hostedDomain) {
+ // We're not initialized until we receive `onConnected`.
+ // If initialization fails, we'll receive `onConnectionFailed`
+ checkAndSetPendingOperation(METHOD_INIT, result);
- /**
- * Returns the account information for the user who is signed in to this app. If no user is signed
- * in, tries to sign the user in without displaying any user interface.
- */
- private void signInSilently(Result result) {
- if (checkAndSetPendingOperation(METHOD_SIGN_IN, result)) {
- return;
+ try {
+ GoogleSignInOptions.Builder optionsBuilder =
+ new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail();
+ // Only requests a clientId if google-services.json was present and parsed
+ // by the google-services Gradle script.
+ // TODO(jackson): Perhaps we should provide a mechanism to override this
+ // behavior.
+ int clientIdIdentifier =
+ activity
+ .getResources()
+ .getIdentifier("default_web_client_id", "string", activity.getPackageName());
+ if (clientIdIdentifier != 0) {
+ optionsBuilder.requestIdToken(activity.getString(clientIdIdentifier));
+ }
+ for (String scope : requestedScopes) {
+ optionsBuilder.requestScopes(new Scope(scope));
+ }
+ if (!Strings.isNullOrEmpty(hostedDomain)) {
+ optionsBuilder.setHostedDomain(hostedDomain);
+ }
+
+ this.requestedScopes = requestedScopes;
+ googleApiClient =
+ new GoogleApiClient.Builder(activity)
+ .addApi(Auth.GOOGLE_SIGN_IN_API, optionsBuilder.build())
+ .addConnectionCallbacks(handler)
+ .addOnConnectionFailedListener(handler)
+ .build();
+ googleApiClient.connect();
+ } catch (Exception e) {
+ Log.e(TAG, "Initialization error", e);
+ result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
+ }
}
- OptionalPendingResult<GoogleSignInResult> pendingResult =
- Auth.GoogleSignInApi.silentSignIn(googleApiClient);
- if (pendingResult.isDone()) {
- onSignInResult(pendingResult.get());
- } else {
- pendingResult.setResultCallback(
- new ResultCallback<GoogleSignInResult>() {
+ /**
+ * Returns the account information for the user who is signed in to this app. If no user is
+ * signed in, tries to sign the user in without displaying any user interface.
+ */
+ public void signInSilently(Result result) {
+ checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result);
+
+ OptionalPendingResult<GoogleSignInResult> pendingResult =
+ Auth.GoogleSignInApi.silentSignIn(googleApiClient);
+ if (pendingResult.isDone()) {
+ onSignInResult(pendingResult.get());
+ } else {
+ pendingResult.setResultCallback(
+ new ResultCallback<GoogleSignInResult>() {
+ @Override
+ public void onResult(@NonNull GoogleSignInResult signInResult) {
+ onSignInResult(signInResult);
+ }
+ });
+ }
+ }
+
+ /**
+ * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes
+ * were requested.
+ */
+ public void signIn(Result result) {
+ checkAndSetPendingOperation(METHOD_SIGN_IN, result);
+
+ Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
+ activity.startActivityForResult(signInIntent, REQUEST_CODE);
+ }
+
+ /**
+ * Gets an OAuth access token with the scopes that were specified during initialization for the
+ * user with the specified email address.
+ */
+ public void getTokens(Result result, final String email) {
+ checkAndSetPendingOperation(METHOD_GET_TOKENS, result);
+ if (email == null) {
+ result.error(ERROR_REASON_EXCEPTION, "Email is null", null);
+ return;
+ }
+
+ Callable<String> getTokenTask =
+ new Callable<String>() {
@Override
- public void onResult(@NonNull GoogleSignInResult signInResult) {
- onSignInResult(signInResult);
+ public String call() throws Exception {
+ Account account = new Account(email, "com.google");
+ String scopesStr = "oauth2:" + Joiner.on(' ').join(requestedScopes);
+ return GoogleAuthUtil.getToken(activity.getApplication(), account, scopesStr);
+ }
+ };
+
+ backgroundTaskRunner.runInBackground(
+ getTokenTask,
+ new BackgroundTaskRunner.Callback<String>() {
+ @Override
+ public void run(Future<String> tokenFuture) {
+ try {
+ String token = tokenFuture.get();
+ HashMap<String, String> result = new HashMap<>();
+ result.put("accessToken", token);
+ // TODO(jackson): If we had a way to get the current user at this
+ // point, we could use that to obtain an up-to-date idToken here
+ // instead of the value we cached during sign in. At least, that's
+ // how it works on iOS.
+ finishWithSuccess(result);
+ } catch (ExecutionException e) {
+ Log.e(TAG, "Exception getting access token", e);
+ finishWithError(ERROR_REASON_EXCEPTION, e.getCause().getMessage());
+ } catch (InterruptedException e) {
+ finishWithError(ERROR_REASON_EXCEPTION, e.getMessage());
+ Thread.currentThread().interrupt();
+ }
}
});
}
- }
- /**
- * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes
- * were requested.
- */
- private void signIn(Result result) {
- if (checkAndSetPendingOperation(METHOD_SIGN_IN, result)) {
- return;
- }
+ /**
+ * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently
+ * sign back in.
+ */
+ public void signOut(Result result) {
+ checkAndSetPendingOperation(METHOD_SIGN_OUT, result);
- Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
- activity.startActivityForResult(signInIntent, requestCode);
- }
-
- /**
- * Gets an OAuth access token with the scopes that were specified during initialization for the
- * user with the specified email address.
- */
- private void getTokens(Result result, final String email) {
- if (email == null) {
- result.error(ERROR_REASON_EXCEPTION, "Email is null", null);
- return;
- }
-
- if (checkAndSetPendingOperation(METHOD_GET_TOKENS, result)) {
- return;
- }
-
- Callable<String> getTokenTask =
- new Callable<String>() {
- @Override
- public String call() throws Exception {
- Account account = new Account(email, "com.google");
- String scopesStr = "oauth2:" + Joiner.on(' ').join(requestedScopes);
- return GoogleAuthUtil.getToken(activity.getApplication(), account, scopesStr);
- }
- };
-
- backgroundTaskRunner.runInBackground(
- getTokenTask,
- new BackgroundTaskRunner.Callback<String>() {
- @Override
- public void run(Future<String> tokenFuture) {
- try {
- String token = tokenFuture.get();
- HashMap<String, String> result = new HashMap<>();
- result.put("accessToken", token);
- // TODO(jackson): If we had a way to get the current user at this
- // point, we could use that to obtain an up-to-date idToken here
- // instead of the value we cached during sign in. At least, that's
- // how it works on iOS.
- finishWithSuccess(result);
- } catch (ExecutionException e) {
- Log.e(TAG, "Exception getting access token", e);
- finishWithError(ERROR_REASON_EXCEPTION, e.getCause().getMessage());
- } catch (InterruptedException e) {
- finishWithError(ERROR_REASON_EXCEPTION, e.getMessage());
- Thread.currentThread().interrupt();
- }
- }
- });
- }
-
- /**
- * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently
- * sign back in.
- */
- private void signOut(Result result) {
- if (checkAndSetPendingOperation(METHOD_SIGN_OUT, result)) {
- return;
- }
-
- Auth.GoogleSignInApi.signOut(googleApiClient)
- .setResultCallback(
- new ResultCallback<Status>() {
- @Override
- public void onResult(@NonNull Status status) {
- // TODO(tvolkert): communicate status back to user
- finishWithSuccess(null);
- }
- });
- }
-
- /** Signs the user out, and revokes their credentials. */
- private void disconnect(Result result) {
- if (checkAndSetPendingOperation(METHOD_DISCONNECT, result)) {
- return;
- }
-
- Auth.GoogleSignInApi.revokeAccess(googleApiClient)
- .setResultCallback(
- new ResultCallback<Status>() {
- @Override
- public void onResult(@NonNull Status status) {
- // TODO(tvolkert): communicate status back to user
- finishWithSuccess(null);
- }
- });
- }
-
- /**
- * Invoked when the GMS client has successfully connected to the GMS server. This signals that
- * this listener is properly initialized.
- */
- @Override
- public void onConnected(Bundle connectionHint) {
- // We can get reconnected if, e.g. the activity is paused and resumed.
- if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
- finishWithSuccess(null);
- }
- }
-
- /**
- * Invoked when the GMS client was unable to connect to the GMS server, either because of an error
- * the user was unable to resolve, or because the user canceled the resolution (e.g. cancelling a
- * dialog instructing them to upgrade Google Play Services). This signals that we were unable to
- * properly initialize this listener.
- */
- @Override
- public void onConnectionFailed(@NonNull final ConnectionResult result) {
- if (resolvingError) {
- // Already attempting to resolve an error.
- return;
- } else if (result.hasResolution()) {
- try {
- resolvingError = true;
- result.startResolutionForResult(activity, REQUEST_CODE_RESOLVE_ERROR);
- } catch (SendIntentException e) {
- resolvingError = false;
- finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
- }
- } else {
- resolvingError = true;
- GoogleApiAvailability.getInstance()
- .showErrorDialogFragment(
- activity,
- result.getErrorCode(),
- REQUEST_CODE_RESOLVE_ERROR,
- new DialogInterface.OnCancelListener() {
+ Auth.GoogleSignInApi.signOut(googleApiClient)
+ .setResultCallback(
+ new ResultCallback<Status>() {
@Override
- public void onCancel(DialogInterface dialog) {
- if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
- finishWithError(
- ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
- }
- resolvingError = false;
+ public void onResult(@NonNull Status status) {
+ // TODO(tvolkert): communicate status back to user
+ finishWithSuccess(null);
}
});
}
- }
- @Override
- public void onConnectionSuspended(int cause) {
- // TODO(jackson): implement
- Log.w(TAG, "The GMS server connection has been suspended (" + cause + ")");
- }
+ /** Signs the user out, and revokes their credentials. */
+ public void disconnect(Result result) {
+ checkAndSetPendingOperation(METHOD_DISCONNECT, result);
- @Override
- public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_CODE_RESOLVE_ERROR) {
- // Deal with result of `onConnectionFailed` error resolution.
- resolvingError = false;
- if (resultCode == Activity.RESULT_OK) {
- // Make sure the app is not already connected or attempting to connect
- if (!googleApiClient.isConnecting() && !googleApiClient.isConnected()) {
+ Auth.GoogleSignInApi.revokeAccess(googleApiClient)
+ .setResultCallback(
+ new ResultCallback<Status>() {
+ @Override
+ public void onResult(@NonNull Status status) {
+ // TODO(tvolkert): communicate status back to user
+ finishWithSuccess(null);
+ }
+ });
+ }
+
+ private void onSignInResult(GoogleSignInResult result) {
+ if (result.isSuccess()) {
+ GoogleSignInAccount account = result.getSignInAccount();
+ Map<String, Object> response = new HashMap<>();
+ response.put("displayName", account.getDisplayName());
+ response.put("email", account.getEmail());
+ response.put("id", account.getId());
+ response.put("idToken", account.getIdToken());
+ Uri photoUrl = account.getPhotoUrl();
+ if (photoUrl != null) {
+ response.put("photoUrl", photoUrl.toString());
+ }
+ finishWithSuccess(response);
+ } else if (result.getStatus().getStatusCode() == CommonStatusCodes.SIGN_IN_REQUIRED
+ || result.getStatus().getStatusCode() == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) {
+ // This isn't an error from the caller's (Dart's) perspective; this just
+ // means that the user didn't sign in.
+ finishWithSuccess(null);
+ } else {
+ finishWithError(ERROR_REASON_STATUS, result.getStatus().toString());
+ }
+ }
+
+ private void finishWithSuccess(Object data) {
+ pendingOperation.result.success(data);
+ pendingOperation = null;
+ }
+
+ private void finishWithError(String errorCode, String errorMessage) {
+ pendingOperation.result.error(errorCode, errorMessage, null);
+ pendingOperation = null;
+ }
+
+ private class PendingOperation {
+ final String method;
+ final Result result;
+
+ PendingOperation(String method, Result result) {
+ this.method = method;
+ this.result = result;
+ }
+ }
+
+ private class Handler
+ implements ActivityLifecycleCallbacks,
+ PluginRegistry.ActivityResultListener,
+ GoogleApiClient.ConnectionCallbacks,
+ GoogleApiClient.OnConnectionFailedListener {
+ @Override
+ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_RESOLVE_ERROR) {
+ // Deal with result of `onConnectionFailed` error resolution.
+ resolvingError = false;
+ if (resultCode == Activity.RESULT_OK) {
+ // Make sure the app is not already connected or attempting to connect
+ if (!googleApiClient.isConnecting() && !googleApiClient.isConnected()) {
+ googleApiClient.connect();
+ }
+ } else if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
+ finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(resultCode));
+ }
+ return true;
+ }
+
+ if (requestCode != REQUEST_CODE) {
+ // We're only interested in the "sign in" activity result
+ return false;
+ }
+
+ if (pendingOperation == null || !pendingOperation.method.equals(METHOD_SIGN_IN)) {
+ Log.w(TAG, "Unexpected activity result; sign-in not in progress");
+ return false;
+ }
+
+ if (data == null) {
+ finishWithError(ERROR_REASON_STATUS, "No intent data: " + resultCode);
+ return true;
+ }
+
+ onSignInResult(Auth.GoogleSignInApi.getSignInResultFromIntent(data));
+ return true;
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle bundle) {
+ resolvingError = bundle != null && bundle.getBoolean(STATE_RESOLVING_ERROR, false);
+ }
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {}
+
+ @Override
+ public void onActivityPaused(Activity activity) {}
+
+ @Override
+ public void onActivityResumed(Activity activity) {}
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+ outState.putBoolean(STATE_RESOLVING_ERROR, resolvingError);
+ }
+
+ @Override
+ public void onActivityStarted(Activity activity) {
+ if (!resolvingError && activity == Delegate.this.activity && googleApiClient != null) {
googleApiClient.connect();
}
- } else if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
- finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(resultCode));
}
- return true;
- }
- if (requestCode != this.requestCode) {
- // We're only interested in the "sign in" activity result
- return false;
- }
-
- if (pendingOperation == null || !pendingOperation.method.equals(METHOD_SIGN_IN)) {
- Log.w(TAG, "Unexpected activity result; sign-in not in progress");
- return false;
- }
-
- if (data == null) {
- finishWithError(ERROR_REASON_STATUS, "No intent data: " + resultCode);
- return true;
- }
-
- onSignInResult(Auth.GoogleSignInApi.getSignInResultFromIntent(data));
- return true;
- }
-
- private void onSignInResult(GoogleSignInResult result) {
- if (result.isSuccess()) {
- GoogleSignInAccount account = result.getSignInAccount();
- Map<String, Object> response = new HashMap<>();
- response.put("displayName", account.getDisplayName());
- response.put("email", account.getEmail());
- response.put("id", account.getId());
- response.put("idToken", account.getIdToken());
- Uri photoUrl = account.getPhotoUrl();
- if (photoUrl != null) {
- response.put("photoUrl", photoUrl.toString());
+ @Override
+ public void onActivityStopped(Activity activity) {
+ if (activity == Delegate.this.activity && googleApiClient != null) {
+ googleApiClient.disconnect();
+ }
}
- finishWithSuccess(response);
- } else if (result.getStatus().getStatusCode() == CommonStatusCodes.SIGN_IN_REQUIRED
- || result.getStatus().getStatusCode() == GoogleSignInStatusCodes.SIGN_IN_CANCELLED) {
- // This isn't an error from the caller's (Dart's) perspective; this just
- // means that the user didn't sign in.
- finishWithSuccess(null);
- } else {
- finishWithError(ERROR_REASON_STATUS, result.getStatus().toString());
- }
- }
- private void finishWithSuccess(Object data) {
- for (Result result : pendingOperation.resultQueue) {
- result.success(data);
- }
- pendingOperation = null;
- }
-
- private void finishWithError(String errorCode, String errorMessage) {
- for (Result result : pendingOperation.resultQueue) {
- result.error(errorCode, errorMessage, null);
- }
- pendingOperation = null;
- }
-
- private class GoogleApiClientConnectionManager implements ActivityLifecycleCallbacks {
- @Override
- public void onActivityCreated(Activity activity, Bundle bundle) {
- resolvingError = bundle != null && bundle.getBoolean(STATE_RESOLVING_ERROR, false);
- }
-
- @Override
- public void onActivityDestroyed(Activity activity) {}
-
- @Override
- public void onActivityPaused(Activity activity) {}
-
- @Override
- public void onActivityResumed(Activity activity) {}
-
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- outState.putBoolean(STATE_RESOLVING_ERROR, resolvingError);
- }
-
- @Override
- public void onActivityStarted(Activity activity) {
- if (!resolvingError
- && activity == GoogleSignInPlugin.this.activity
- && googleApiClient != null) {
- googleApiClient.connect();
+ /**
+ * Invoked when the GMS client has successfully connected to the GMS server. This signals that
+ * this listener is properly initialized.
+ */
+ @Override
+ public void onConnected(Bundle connectionHint) {
+ // We can get reconnected if, e.g. the activity is paused and resumed.
+ if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
+ finishWithSuccess(null);
+ }
}
- }
- @Override
- public void onActivityStopped(Activity activity) {
- if (activity == GoogleSignInPlugin.this.activity && googleApiClient != null) {
- googleApiClient.disconnect();
+ /**
+ * Invoked when the GMS client was unable to connect to the GMS server, either because of an
+ * error the user was unable to resolve, or because the user canceled the resolution (e.g.
+ * cancelling a dialog instructing them to upgrade Google Play Services). This signals that we
+ * were unable to properly initialize this listener.
+ */
+ @Override
+ public void onConnectionFailed(@NonNull final ConnectionResult result) {
+ if (resolvingError) {
+ // Already attempting to resolve an error.
+ return;
+ } else if (result.hasResolution()) {
+ resolvingError = true;
+ try {
+ result.startResolutionForResult(activity, REQUEST_CODE_RESOLVE_ERROR);
+ } catch (SendIntentException e) {
+ resolvingError = false;
+ finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
+ }
+ } else {
+ resolvingError = true;
+ GoogleApiAvailability.getInstance()
+ .showErrorDialogFragment(
+ activity,
+ result.getErrorCode(),
+ REQUEST_CODE_RESOLVE_ERROR,
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
+ finishWithError(
+ ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
+ }
+ resolvingError = false;
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onConnectionSuspended(int cause) {
+ // TODO(jackson): implement
+ Log.w(TAG, "The GMS server connection has been suspended (" + cause + ")");
}
}
}