From 6ab9a8bfb4c7151047df882c1d9ed1a68d229d0a Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Fri, 30 Jun 2023 20:28:49 -0400 Subject: [PATCH] [google_sign_in] Convert Android to Pigeon (#4344) Replaces the direct method channel implementation with Pigeon. Since `google_sign_in`, unlike most of our plugins, exposes an API that's intended for direct cross-plugin native use, the existing methods are all left in place, but refactored as passthroughs to the new Pigeon versions. To ensure that they aren't broken, the existing Java tests are preserved unchanged (as a "legacy" copy) with `onMethodCall` left in place for now just to allow the tests to continue to run as-is. Since that dispatches to the legacy methods, this keeps the existing coverage of those methods. The new tests are a copy of the legacy tests, minimally translated to use the new Pigeon variants, to ensure continuity of testing to the new version. Part of https://github.com/flutter/flutter/issues/117908 --- .../google_sign_in_android/CHANGELOG.md | 4 + .../googlesignin/GoogleSignInPlugin.java | 493 ++++++++---- .../plugins/googlesignin/Messages.java | 712 ++++++++++++++++++ .../GoogleSignInLegacyMethodChannelTest.java | 378 ++++++++++ .../googlesignin/GoogleSignInTest.java | 188 +++-- .../lib/google_sign_in_android.dart | 94 ++- .../lib/src/messages.g.dart | 387 ++++++++++ .../google_sign_in_android/lib/src/utils.dart | 28 - .../pigeons/copyright.txt | 3 + .../pigeons/messages.dart | 106 +++ .../google_sign_in_android/pubspec.yaml | 5 +- .../test/google_sign_in_android_test.dart | 286 +++---- .../google_sign_in_android_test.mocks.dart | 138 ++++ 13 files changed, 2394 insertions(+), 428 deletions(-) create mode 100644 packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java create mode 100644 packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java create mode 100644 packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart delete mode 100644 packages/google_sign_in/google_sign_in_android/lib/src/utils.dart create mode 100644 packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt create mode 100644 packages/google_sign_in/google_sign_in_android/pigeons/messages.dart create mode 100644 packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart diff --git a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md index 2d9aae4ff7..499856fe9a 100644 --- a/packages/google_sign_in/google_sign_in_android/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 6.1.17 + +* Converts method channels to Pigeon. + ## 6.1.16 * Updates Guava to version 32.0.1. diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java index d27cd5e116..eb4635a817 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -31,11 +31,10 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.googlesignin.Messages.FlutterError; +import io.flutter.plugins.googlesignin.Messages.GoogleSignInApi; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -45,21 +44,9 @@ import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; /** Google sign-in plugin for Flutter. */ -public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { - private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in_android"; - - private static final String METHOD_INIT = "init"; - private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently"; - private static final String METHOD_SIGN_IN = "signIn"; - private static final String METHOD_GET_TOKENS = "getTokens"; - private static final String METHOD_SIGN_OUT = "signOut"; - private static final String METHOD_DISCONNECT = "disconnect"; - private static final String METHOD_IS_SIGNED_IN = "isSignedIn"; - private static final String METHOD_CLEAR_AUTH_CACHE = "clearAuthCache"; - private static final String METHOD_REQUEST_SCOPES = "requestScopes"; - +public class GoogleSignInPlugin implements FlutterPlugin, ActivityAware { private Delegate delegate; - private MethodChannel channel; + private @Nullable BinaryMessenger messenger; private ActivityPluginBinding activityPluginBinding; @SuppressWarnings("deprecation") @@ -75,9 +62,9 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act @NonNull BinaryMessenger messenger, @NonNull Context context, @NonNull GoogleSignInWrapper googleSignInWrapper) { - channel = new MethodChannel(messenger, CHANNEL_NAME); + this.messenger = messenger; delegate = new Delegate(context, googleSignInWrapper); - channel.setMethodCallHandler(this); + GoogleSignInApi.setup(messenger, delegate); } @VisibleForTesting @@ -88,8 +75,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act private void dispose() { delegate = null; - channel.setMethodCallHandler(null); - channel = null; + if (messenger != null) { + GoogleSignInApi.setup(messenger, null); + messenger = null; + } } private void attachToActivity(ActivityPluginBinding activityPluginBinding) { @@ -136,10 +125,15 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act disposeActivity(); } - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { + // TODO(stuartmorgan): Remove this, and convert the unit tests to IDelegate tests. This is left + // here only to allow the existing tests to continue to work unchanged during the Pigeon migration + // to ensure that the refactoring didn't change any behavior, and is not actually used by the + // plugin. + @VisibleForTesting + void onMethodCall( + @NonNull io.flutter.plugin.common.MethodCall call, @NonNull MethodChannel.Result result) { switch (call.method) { - case METHOD_INIT: + case "init": String signInOption = Objects.requireNonNull(call.argument("signInOption")); List requestedScopes = Objects.requireNonNull(call.argument("scopes")); String hostedDomain = call.argument("hostedDomain"); @@ -157,38 +151,38 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act forceCodeForRefreshToken); break; - case METHOD_SIGN_IN_SILENTLY: + case "signInSilently": delegate.signInSilently(result); break; - case METHOD_SIGN_IN: + case "signIn": delegate.signIn(result); break; - case METHOD_GET_TOKENS: + case "getTokens": String email = Objects.requireNonNull(call.argument("email")); boolean shouldRecoverAuth = Objects.requireNonNull(call.argument("shouldRecoverAuth")); delegate.getTokens(result, email, shouldRecoverAuth); break; - case METHOD_SIGN_OUT: + case "signOut": delegate.signOut(result); break; - case METHOD_CLEAR_AUTH_CACHE: + case "clearAuthCache": String token = Objects.requireNonNull(call.argument("token")); delegate.clearAuthCache(result, token); break; - case METHOD_DISCONNECT: + case "disconnect": delegate.disconnect(result); break; - case METHOD_IS_SIGNED_IN: + case "isSignedIn": delegate.isSignedIn(result); break; - case METHOD_REQUEST_SCOPES: + case "requestScopes": List scopes = Objects.requireNonNull(call.argument("scopes")); delegate.requestScopes(result, scopes); break; @@ -206,7 +200,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act public interface IDelegate { /** Initializes this delegate so that it is ready to perform other operations. */ void init( - @NonNull Result result, + @NonNull MethodChannel.Result result, @NonNull String signInOption, @NonNull List requestedScopes, @Nullable String hostedDomain, @@ -218,13 +212,13 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * 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. */ - void signInSilently(@NonNull Result result); + void signInSilently(@NonNull MethodChannel.Result result); /** * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes * were requested. */ - void signIn(@NonNull Result result); + void signIn(@NonNull MethodChannel.Result result); /** * Gets an OAuth access token with the scopes that were specified during initialization for the @@ -234,28 +228,97 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * complete, the method will attempt to recover authentication and rerun method. */ void getTokens( - final @NonNull Result result, final @NonNull String email, final boolean shouldRecoverAuth); + final @NonNull MethodChannel.Result result, + final @NonNull String email, + final boolean shouldRecoverAuth); /** * Clears the token from any client cache forcing the next {@link #getTokens} call to fetch a * new one. */ - void clearAuthCache(final @NonNull Result result, final @NonNull String token); + void clearAuthCache(final @NonNull MethodChannel.Result result, final @NonNull String token); /** * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently * sign back in. */ - void signOut(@NonNull Result result); + void signOut(@NonNull MethodChannel.Result result); /** Signs the user out, and revokes their credentials. */ - void disconnect(@NonNull Result result); + void disconnect(@NonNull MethodChannel.Result result); /** Checks if there is a signed in user. */ - void isSignedIn(@NonNull Result result); + void isSignedIn(@NonNull MethodChannel.Result result); /** Prompts the user to grant an additional Oauth scopes. */ - void requestScopes(final @NonNull Result result, final @NonNull List scopes); + void requestScopes( + final @NonNull MethodChannel.Result result, final @NonNull List scopes); + } + + /** + * Helper class for supporting the legacy IDelegate interface based on raw method channels, which + * handles converting any FlutterErrors (or other {@code Throwable}s in case any non- FlutterError + * exceptions slip through) thrown by the new code paths into {@code error} callbacks. + * + * @param The Result type of the result to convert from. + */ + private abstract static class ErrorConvertingMethodChannelResult + implements Messages.Result { + final @NonNull MethodChannel.Result result; + + public ErrorConvertingMethodChannelResult(@NonNull MethodChannel.Result result) { + this.result = result; + } + + @Override + public void error(@NonNull Throwable error) { + if (error instanceof FlutterError) { + FlutterError flutterError = (FlutterError) error; + result.error(flutterError.code, flutterError.getMessage(), flutterError.details); + } else { + result.error("exception", error.getMessage(), null); + } + } + } + + /** + * Helper class for supporting the legacy IDelegate interface based on raw method channels, which + * handles converting responses from methods that return {@code Messages.UserData}. + */ + private static class UserDataMethodChannelResult + extends ErrorConvertingMethodChannelResult { + public UserDataMethodChannelResult(MethodChannel.Result result) { + super(result); + } + + @Override + public void success(Messages.UserData data) { + Map response = new HashMap<>(); + response.put("email", data.getEmail()); + response.put("id", data.getId()); + response.put("idToken", data.getIdToken()); + response.put("serverAuthCode", data.getServerAuthCode()); + response.put("displayName", data.getDisplayName()); + if (data.getPhotoUrl() != null) { + response.put("photoUrl", data.getPhotoUrl()); + } + result.success(response); + } + } + + /** + * Helper class for supporting the legacy IDelegate interface based on raw method channels, which + * handles converting responses from methods that return {@code Void}. + */ + private static class VoidMethodChannelResult extends ErrorConvertingMethodChannelResult { + public VoidMethodChannelResult(MethodChannel.Result result) { + super(result); + } + + @Override + public void success(Void unused) { + result.success(null); + } } /** @@ -263,11 +326,16 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * class for use in other plugins that wrap basic sign-in functionality. * *

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 constructs - * to guarantee such behavior; callers are responsible for providing such guarantees. + * invoked. In this context, "run to completion" means that their {@link MethodChannel.Result} + * argument has been completed (either successfully or in error). This class provides no + * synchronization constructs to guarantee such behavior; callers are responsible for providing + * such guarantees. */ - public static class Delegate implements IDelegate, PluginRegistry.ActivityResultListener { + // TODO(stuartmorgan): Remove this in a breaking change, replacing it with something using + // structured types rather than strings and dictionaries left over from the pre-Pigeon method + // channel implementation. + public static class Delegate + implements IDelegate, PluginRegistry.ActivityResultListener, GoogleSignInApi { private static final int REQUEST_CODE_SIGNIN = 53293; private static final int REQUEST_CODE_RECOVER_AUTH = 53294; @VisibleForTesting static final int REQUEST_CODE_REQUEST_SCOPE = 53295; @@ -291,6 +359,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act private PluginRegistry.Registrar registrar; // Only set activity for v2 embedder. Always access activity from getActivity() method. private @Nullable Activity activity; + // TODO(stuartmorgan): See whether this can be replaced with background channels. private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1); private final GoogleSignInWrapper googleSignInWrapper; @@ -318,16 +387,44 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act return registrar != null ? registrar.activity() : activity; } - private void checkAndSetPendingOperation(String method, Result result) { - checkAndSetPendingOperation(method, result, null); - } - - private void checkAndSetPendingOperation(String method, Result result, Object data) { + private void checkAndSetPendingOperation( + String method, + Messages.Result userDataResult, + Messages.Result voidResult, + Messages.Result boolResult, + Messages.Result stringResult, + Object data) { if (pendingOperation != null) { throw new IllegalStateException( "Concurrent operations detected: " + pendingOperation.method + ", " + method); } - pendingOperation = new PendingOperation(method, result, data); + pendingOperation = + new PendingOperation(method, userDataResult, voidResult, boolResult, stringResult, data); + } + + private void checkAndSetPendingSignInOperation( + String method, @NonNull Messages.Result result) { + checkAndSetPendingOperation(method, result, null, null, null, null); + } + + private void checkAndSetPendingVoidOperation( + String method, @NonNull Messages.Result result) { + checkAndSetPendingOperation(method, null, result, null, null, null); + } + + private void checkAndSetPendingBoolOperation( + String method, @NonNull Messages.Result result) { + checkAndSetPendingOperation(method, null, null, result, null, null); + } + + private void checkAndSetPendingStringOperation( + String method, @NonNull Messages.Result result, @Nullable Object data) { + checkAndSetPendingOperation(method, null, null, null, result, data); + } + + private void checkAndSetPendingAccessTokenOperation( + String method, Messages.Result result, @NonNull Object data) { + checkAndSetPendingStringOperation(method, result, data); } /** @@ -335,23 +432,16 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * guarantees that this will be called and completed before any other methods are invoked. */ @Override - public void init( - @NonNull Result result, - @NonNull String signInOption, - @NonNull List requestedScopes, - @Nullable String hostedDomain, - @Nullable String clientId, - @Nullable String serverClientId, - boolean forceCodeForRefreshToken) { + public void init(@NonNull Messages.InitParams params) { try { GoogleSignInOptions.Builder optionsBuilder; - switch (signInOption) { - case DEFAULT_GAMES_SIGN_IN: + switch (params.getSignInType()) { + case GAMES: optionsBuilder = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_GAMES_SIGN_IN); break; - case DEFAULT_SIGN_IN: + case STANDARD: optionsBuilder = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN).requestEmail(); break; @@ -363,12 +453,13 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act // Android apps are identified by their package name and the SHA-1 of their signing key. // https://developers.google.com/android/guides/client-auth // https://developers.google.com/identity/sign-in/android/start#configure-a-google-api-project - if (!Strings.isNullOrEmpty(clientId) && Strings.isNullOrEmpty(serverClientId)) { + String serverClientId = params.getServerClientId(); + if (!Strings.isNullOrEmpty(params.getClientId()) && Strings.isNullOrEmpty(serverClientId)) { Log.w( "google_sign_in", "clientId is not supported on Android and is interpreted as serverClientId. " + "Use serverClientId instead to suppress this warning."); - serverClientId = clientId; + serverClientId = params.getClientId(); } if (Strings.isNullOrEmpty(serverClientId)) { @@ -387,20 +478,57 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } if (!Strings.isNullOrEmpty(serverClientId)) { optionsBuilder.requestIdToken(serverClientId); - optionsBuilder.requestServerAuthCode(serverClientId, forceCodeForRefreshToken); + optionsBuilder.requestServerAuthCode( + serverClientId, params.getForceCodeForRefreshToken()); } + requestedScopes = params.getScopes(); for (String scope : requestedScopes) { optionsBuilder.requestScopes(new Scope(scope)); } - if (!Strings.isNullOrEmpty(hostedDomain)) { - optionsBuilder.setHostedDomain(hostedDomain); + if (!Strings.isNullOrEmpty(params.getHostedDomain())) { + optionsBuilder.setHostedDomain(params.getHostedDomain()); } - this.requestedScopes = requestedScopes; signInClient = googleSignInWrapper.getClient(context, optionsBuilder.build()); - result.success(null); } catch (Exception e) { - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + throw new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null); + } + } + + // IDelegate version, for backwards compatibility. + @Override + public void init( + @NonNull MethodChannel.Result result, + @NonNull String signInOption, + @NonNull List requestedScopes, + @Nullable String hostedDomain, + @Nullable String clientId, + @Nullable String serverClientId, + boolean forceCodeForRefreshToken) { + try { + Messages.SignInType type; + switch (signInOption) { + case DEFAULT_GAMES_SIGN_IN: + type = Messages.SignInType.GAMES; + break; + case DEFAULT_SIGN_IN: + type = Messages.SignInType.STANDARD; + break; + default: + throw new IllegalStateException("Unknown signInOption"); + } + init( + new Messages.InitParams.Builder() + .setSignInType(type) + .setScopes(requestedScopes) + .setHostedDomain(hostedDomain) + .setClientId(clientId) + .setServerClientId(serverClientId) + .setForceCodeForRefreshToken(forceCodeForRefreshToken) + .build()); + result.success(null); + } catch (FlutterError e) { + result.error(e.code, e.getMessage(), e.details); } } @@ -409,8 +537,8 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * signed in, tries to sign the user in without displaying any user interface. */ @Override - public void signInSilently(@NonNull Result result) { - checkAndSetPendingOperation(METHOD_SIGN_IN_SILENTLY, result); + public void signInSilently(@NonNull Messages.Result result) { + checkAndSetPendingSignInOperation("signInSilently", result); Task task = signInClient.silentSignIn(); if (task.isComplete()) { // There's immediate result available. @@ -420,68 +548,99 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } } + // IDelegate version, for backwards compatibility. + @Override + public void signInSilently(@NonNull MethodChannel.Result result) { + signInSilently(new UserDataMethodChannelResult(result)); + } + /** * Signs the user in via the sign-in user interface, including the OAuth consent flow if scopes * were requested. */ @Override - public void signIn(@NonNull Result result) { + public void signIn(@NonNull Messages.Result result) { if (getActivity() == null) { throw new IllegalStateException("signIn needs a foreground activity"); } - checkAndSetPendingOperation(METHOD_SIGN_IN, result); + checkAndSetPendingSignInOperation("signIn", result); Intent signInIntent = signInClient.getSignInIntent(); getActivity().startActivityForResult(signInIntent, REQUEST_CODE_SIGNIN); } + // IDelegate version, for backwards compatibility. + @Override + public void signIn(@NonNull MethodChannel.Result result) { + signIn(new UserDataMethodChannelResult(result)); + } + /** * Signs the user out. Their credentials may remain valid, meaning they'll be able to silently * sign back in. */ @Override - public void signOut(@NonNull Result result) { - checkAndSetPendingOperation(METHOD_SIGN_OUT, result); + public void signOut(@NonNull Messages.Result result) { + checkAndSetPendingVoidOperation("signOut", result); signInClient .signOut() .addOnCompleteListener( task -> { if (task.isSuccessful()) { - finishWithSuccess(null); + finishWithSuccess(); } else { finishWithError(ERROR_REASON_STATUS, "Failed to signout."); } }); } + // IDelegate version, for backwards compatibility. + @Override + public void signOut(@NonNull MethodChannel.Result result) { + signOut(new VoidMethodChannelResult(result)); + } + /** Signs the user out, and revokes their credentials. */ @Override - public void disconnect(@NonNull Result result) { - checkAndSetPendingOperation(METHOD_DISCONNECT, result); + public void disconnect(@NonNull Messages.Result result) { + checkAndSetPendingVoidOperation("disconnect", result); signInClient .revokeAccess() .addOnCompleteListener( task -> { if (task.isSuccessful()) { - finishWithSuccess(null); + finishWithSuccess(); } else { finishWithError(ERROR_REASON_STATUS, "Failed to disconnect."); } }); } - /** Checks if there is a signed in user. */ + // IDelegate version, for backwards compatibility. @Override - public void isSignedIn(final @NonNull Result result) { - boolean value = GoogleSignIn.getLastSignedInAccount(context) != null; - result.success(value); + public void disconnect(@NonNull MethodChannel.Result result) { + signOut(new VoidMethodChannelResult(result)); + } + + /** Checks if there is a signed in user. */ + @NonNull + @Override + public Boolean isSignedIn() { + return GoogleSignIn.getLastSignedInAccount(context) != null; + } + + // IDelegate version, for backwards compatibility. + @Override + public void isSignedIn(final @NonNull MethodChannel.Result result) { + result.success(isSignedIn()); } @Override - public void requestScopes(@NonNull Result result, @NonNull List scopes) { - checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result); + public void requestScopes( + @NonNull List scopes, @NonNull Messages.Result result) { + checkAndSetPendingBoolOperation("requestScopes", result); GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(context); if (account == null) { @@ -499,7 +658,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } if (wrappedScopes.isEmpty()) { - finishWithSuccess(true); + finishWithBoolean(true); return; } @@ -507,6 +666,19 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act getActivity(), REQUEST_CODE_REQUEST_SCOPE, account, wrappedScopes.toArray(new Scope[0])); } + // IDelegate version, for backwards compatibility. + @Override + public void requestScopes(@NonNull MethodChannel.Result result, @NonNull List scopes) { + requestScopes( + scopes, + new ErrorConvertingMethodChannelResult(result) { + @Override + public void success(Boolean value) { + result.success(value); + } + }); + } + private void onSignInResult(Task completedTask) { try { GoogleSignInAccount account = completedTask.getResult(ApiException.class); @@ -521,16 +693,21 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } private void onSignInAccount(GoogleSignInAccount account) { - Map response = new HashMap<>(); - response.put("email", account.getEmail()); - response.put("id", account.getId()); - response.put("idToken", account.getIdToken()); - response.put("serverAuthCode", account.getServerAuthCode()); - response.put("displayName", account.getDisplayName()); + final Messages.UserData.Builder builder = + new Messages.UserData.Builder() + // TODO(stuartmorgan): Test with games sign-in; according to docs these could be null + // as the games login request is currently constructed, but the public Dart API + // assumes they are non-null, so the sign-in query may need to change to + // include requestEmail() and requestProfile(). + .setEmail(account.getEmail()) + .setId(account.getId()) + .setIdToken(account.getIdToken()) + .setServerAuthCode(account.getServerAuthCode()) + .setDisplayName(account.getDisplayName()); if (account.getPhotoUrl() != null) { - response.put("photoUrl", account.getPhotoUrl().toString()); + builder.setPhotoUrl(account.getPhotoUrl().toString()); } - finishWithSuccess(response); + finishWithUserData(builder.build()); } private String errorCodeForStatus(int statusCode) { @@ -550,31 +727,67 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } } - private void finishWithSuccess(Object data) { - pendingOperation.result.success(data); + private void finishWithSuccess() { + Objects.requireNonNull(pendingOperation.voidResult).success(null); + pendingOperation = null; + } + + private void finishWithBoolean(Boolean value) { + Objects.requireNonNull(pendingOperation.boolResult).success(value); + pendingOperation = null; + } + + private void finishWithUserData(Messages.UserData data) { + Objects.requireNonNull(pendingOperation.userDataResult).success(data); pendingOperation = null; } private void finishWithError(String errorCode, String errorMessage) { - pendingOperation.result.error(errorCode, errorMessage, null); + Messages.Result result; + if (pendingOperation.userDataResult != null) { + result = pendingOperation.userDataResult; + } else if (pendingOperation.boolResult != null) { + result = pendingOperation.boolResult; + } else if (pendingOperation.stringResult != null) { + result = pendingOperation.stringResult; + } else { + result = pendingOperation.voidResult; + } + Objects.requireNonNull(result).error(new FlutterError(errorCode, errorMessage, null)); pendingOperation = null; } private static class PendingOperation { - final String method; - final Result result; - final Object data; + final @NonNull String method; + final @Nullable Messages.Result userDataResult; + final @Nullable Messages.Result voidResult; + final @Nullable Messages.Result boolResult; + final @Nullable Messages.Result stringResult; + final @Nullable Object data; - PendingOperation(String method, Result result, Object data) { + PendingOperation( + @NonNull String method, + @Nullable Messages.Result userDataResult, + @Nullable Messages.Result voidResult, + @Nullable Messages.Result boolResult, + @Nullable Messages.Result stringResult, + @Nullable Object data) { + assert (userDataResult != null + || voidResult != null + || boolResult != null + || stringResult != null); this.method = method; - this.result = result; + this.userDataResult = userDataResult; + this.voidResult = voidResult; + this.boolResult = boolResult; + this.stringResult = stringResult; this.data = data; } } /** Clears the token kept in the client side cache. */ @Override - public void clearAuthCache(final @NonNull Result result, final @NonNull String token) { + public void clearAuthCache(@NonNull String token, @NonNull Messages.Result result) { Callable clearTokenTask = () -> { GoogleAuthUtil.clearToken(context, token); @@ -588,14 +801,23 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act result.success(clearTokenFuture.get()); } catch (ExecutionException e) { @Nullable Throwable cause = e.getCause(); - result.error(ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null); + result.error( + new FlutterError( + ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null)); } catch (InterruptedException e) { - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + result.error(new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null)); Thread.currentThread().interrupt(); } }); } + // IDelegate version, for backwards compatibility. + @Override + public void clearAuthCache( + final @NonNull MethodChannel.Result result, final @NonNull String token) { + clearAuthCache(token, new VoidMethodChannelResult(result)); + } + /** * Gets an OAuth access token with the scopes that were specified during initialization for the * user with the specified email address. @@ -604,10 +826,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act * complete, the method will attempt to recover authentication and rerun method. */ @Override - public void getTokens( - @NonNull final Result result, - @NonNull final String email, - final boolean shouldRecoverAuth) { + public void getAccessToken( + @NonNull String email, + @NonNull Boolean shouldRecoverAuth, + @NonNull Messages.Result result) { Callable getTokenTask = () -> { Account account = new Account(email, "com.google"); @@ -622,41 +844,60 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act getTokenTask, tokenFuture -> { try { - String token = tokenFuture.get(); - HashMap tokenResult = new HashMap<>(); - tokenResult.put("accessToken", token); - result.success(tokenResult); + result.success(tokenFuture.get()); } catch (ExecutionException e) { if (e.getCause() instanceof UserRecoverableAuthException) { if (shouldRecoverAuth && pendingOperation == null) { Activity activity = getActivity(); if (activity == null) { result.error( - ERROR_USER_RECOVERABLE_AUTH, - "Cannot recover auth because app is not in foreground. " - + e.getLocalizedMessage(), - null); + new FlutterError( + ERROR_USER_RECOVERABLE_AUTH, + "Cannot recover auth because app is not in foreground. " + + e.getLocalizedMessage(), + null)); } else { - checkAndSetPendingOperation(METHOD_GET_TOKENS, result, email); + checkAndSetPendingAccessTokenOperation("getTokens", result, email); Intent recoveryIntent = ((UserRecoverableAuthException) e.getCause()).getIntent(); activity.startActivityForResult(recoveryIntent, REQUEST_CODE_RECOVER_AUTH); } } else { - result.error(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null); + result.error( + new FlutterError(ERROR_USER_RECOVERABLE_AUTH, e.getLocalizedMessage(), null)); } } else { @Nullable Throwable cause = e.getCause(); result.error( - ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null); + new FlutterError( + ERROR_REASON_EXCEPTION, cause == null ? null : cause.getMessage(), null)); } } catch (InterruptedException e) { - result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null); + result.error(new FlutterError(ERROR_REASON_EXCEPTION, e.getMessage(), null)); Thread.currentThread().interrupt(); } }); } + // IDelegate version, for backwards compatibility. + @Override + public void getTokens( + @NonNull final MethodChannel.Result result, + @NonNull final String email, + final boolean shouldRecoverAuth) { + getAccessToken( + email, + shouldRecoverAuth, + new ErrorConvertingMethodChannelResult(result) { + @Override + public void success(String value) { + HashMap tokenResult = new HashMap<>(); + tokenResult.put("accessToken", value); + result.success(tokenResult); + } + }); + } + @Override public boolean onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (pendingOperation == null) { @@ -666,10 +907,10 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act case REQUEST_CODE_RECOVER_AUTH: if (resultCode == Activity.RESULT_OK) { // Recover the previous result and data and attempt to get tokens again. - Result result = pendingOperation.result; - String email = (String) pendingOperation.data; + Messages.Result result = Objects.requireNonNull(pendingOperation.stringResult); + String email = (String) Objects.requireNonNull(pendingOperation.data); pendingOperation = null; - getTokens(result, email, false); + getAccessToken(email, false, result); } else { finishWithError( ERROR_FAILURE_TO_RECOVER_AUTH, "Failed attempt to recover authentication"); @@ -686,7 +927,7 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act } return true; case REQUEST_CODE_REQUEST_SCOPE: - finishWithSuccess(resultCode == Activity.RESULT_OK); + finishWithBoolean(resultCode == Activity.RESULT_OK); return true; default: return false; diff --git a/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java new file mode 100644 index 0000000000..aa94bbbf72 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/main/java/io/flutter/plugins/googlesignin/Messages.java @@ -0,0 +1,712 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v10.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +package io.flutter.plugins.googlesignin; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.BasicMessageChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StandardMessageCodec; +import java.io.ByteArrayOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** Generated class from Pigeon. */ +@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"}) +public class Messages { + + /** Error class for passing custom error details to Flutter via a thrown PlatformException. */ + public static class FlutterError extends RuntimeException { + + /** The error code. */ + public final String code; + + /** The error details. Must be a datatype supported by the api codec. */ + public final Object details; + + public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details) { + super(message); + this.code = code; + this.details = details; + } + } + + @NonNull + protected static ArrayList wrapError(@NonNull Throwable exception) { + ArrayList errorList = new ArrayList(3); + if (exception instanceof FlutterError) { + FlutterError error = (FlutterError) exception; + errorList.add(error.code); + errorList.add(error.getMessage()); + errorList.add(error.details); + } else { + errorList.add(exception.toString()); + errorList.add(exception.getClass().getSimpleName()); + errorList.add( + "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception)); + } + return errorList; + } + + /** Pigeon version of SignInOption. */ + public enum SignInType { + /** Default configuration. */ + STANDARD(0), + /** Recommended configuration for game sign in. */ + GAMES(1); + + final int index; + + private SignInType(final int index) { + this.index = index; + } + } + + /** + * Pigeon version of SignInInitParams. + * + *

See SignInInitParams for details. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class InitParams { + private @NonNull List scopes; + + public @NonNull List getScopes() { + return scopes; + } + + public void setScopes(@NonNull List setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"scopes\" is null."); + } + this.scopes = setterArg; + } + + private @NonNull SignInType signInType; + + public @NonNull SignInType getSignInType() { + return signInType; + } + + public void setSignInType(@NonNull SignInType setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"signInType\" is null."); + } + this.signInType = setterArg; + } + + private @Nullable String hostedDomain; + + public @Nullable String getHostedDomain() { + return hostedDomain; + } + + public void setHostedDomain(@Nullable String setterArg) { + this.hostedDomain = setterArg; + } + + private @Nullable String clientId; + + public @Nullable String getClientId() { + return clientId; + } + + public void setClientId(@Nullable String setterArg) { + this.clientId = setterArg; + } + + private @Nullable String serverClientId; + + public @Nullable String getServerClientId() { + return serverClientId; + } + + public void setServerClientId(@Nullable String setterArg) { + this.serverClientId = setterArg; + } + + private @NonNull Boolean forceCodeForRefreshToken; + + public @NonNull Boolean getForceCodeForRefreshToken() { + return forceCodeForRefreshToken; + } + + public void setForceCodeForRefreshToken(@NonNull Boolean setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"forceCodeForRefreshToken\" is null."); + } + this.forceCodeForRefreshToken = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + InitParams() {} + + public static final class Builder { + + private @Nullable List scopes; + + public @NonNull Builder setScopes(@NonNull List setterArg) { + this.scopes = setterArg; + return this; + } + + private @Nullable SignInType signInType; + + public @NonNull Builder setSignInType(@NonNull SignInType setterArg) { + this.signInType = setterArg; + return this; + } + + private @Nullable String hostedDomain; + + public @NonNull Builder setHostedDomain(@Nullable String setterArg) { + this.hostedDomain = setterArg; + return this; + } + + private @Nullable String clientId; + + public @NonNull Builder setClientId(@Nullable String setterArg) { + this.clientId = setterArg; + return this; + } + + private @Nullable String serverClientId; + + public @NonNull Builder setServerClientId(@Nullable String setterArg) { + this.serverClientId = setterArg; + return this; + } + + private @Nullable Boolean forceCodeForRefreshToken; + + public @NonNull Builder setForceCodeForRefreshToken(@NonNull Boolean setterArg) { + this.forceCodeForRefreshToken = setterArg; + return this; + } + + public @NonNull InitParams build() { + InitParams pigeonReturn = new InitParams(); + pigeonReturn.setScopes(scopes); + pigeonReturn.setSignInType(signInType); + pigeonReturn.setHostedDomain(hostedDomain); + pigeonReturn.setClientId(clientId); + pigeonReturn.setServerClientId(serverClientId); + pigeonReturn.setForceCodeForRefreshToken(forceCodeForRefreshToken); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(scopes); + toListResult.add(signInType == null ? null : signInType.index); + toListResult.add(hostedDomain); + toListResult.add(clientId); + toListResult.add(serverClientId); + toListResult.add(forceCodeForRefreshToken); + return toListResult; + } + + static @NonNull InitParams fromList(@NonNull ArrayList list) { + InitParams pigeonResult = new InitParams(); + Object scopes = list.get(0); + pigeonResult.setScopes((List) scopes); + Object signInType = list.get(1); + pigeonResult.setSignInType(signInType == null ? null : SignInType.values()[(int) signInType]); + Object hostedDomain = list.get(2); + pigeonResult.setHostedDomain((String) hostedDomain); + Object clientId = list.get(3); + pigeonResult.setClientId((String) clientId); + Object serverClientId = list.get(4); + pigeonResult.setServerClientId((String) serverClientId); + Object forceCodeForRefreshToken = list.get(5); + pigeonResult.setForceCodeForRefreshToken((Boolean) forceCodeForRefreshToken); + return pigeonResult; + } + } + + /** + * Pigeon version of GoogleSignInUserData. + * + *

See GoogleSignInUserData for details. + * + *

Generated class from Pigeon that represents data sent in messages. + */ + public static final class UserData { + private @Nullable String displayName; + + public @Nullable String getDisplayName() { + return displayName; + } + + public void setDisplayName(@Nullable String setterArg) { + this.displayName = setterArg; + } + + private @NonNull String email; + + public @NonNull String getEmail() { + return email; + } + + public void setEmail(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"email\" is null."); + } + this.email = setterArg; + } + + private @NonNull String id; + + public @NonNull String getId() { + return id; + } + + public void setId(@NonNull String setterArg) { + if (setterArg == null) { + throw new IllegalStateException("Nonnull field \"id\" is null."); + } + this.id = setterArg; + } + + private @Nullable String photoUrl; + + public @Nullable String getPhotoUrl() { + return photoUrl; + } + + public void setPhotoUrl(@Nullable String setterArg) { + this.photoUrl = setterArg; + } + + private @Nullable String idToken; + + public @Nullable String getIdToken() { + return idToken; + } + + public void setIdToken(@Nullable String setterArg) { + this.idToken = setterArg; + } + + private @Nullable String serverAuthCode; + + public @Nullable String getServerAuthCode() { + return serverAuthCode; + } + + public void setServerAuthCode(@Nullable String setterArg) { + this.serverAuthCode = setterArg; + } + + /** Constructor is non-public to enforce null safety; use Builder. */ + UserData() {} + + public static final class Builder { + + private @Nullable String displayName; + + public @NonNull Builder setDisplayName(@Nullable String setterArg) { + this.displayName = setterArg; + return this; + } + + private @Nullable String email; + + public @NonNull Builder setEmail(@NonNull String setterArg) { + this.email = setterArg; + return this; + } + + private @Nullable String id; + + public @NonNull Builder setId(@NonNull String setterArg) { + this.id = setterArg; + return this; + } + + private @Nullable String photoUrl; + + public @NonNull Builder setPhotoUrl(@Nullable String setterArg) { + this.photoUrl = setterArg; + return this; + } + + private @Nullable String idToken; + + public @NonNull Builder setIdToken(@Nullable String setterArg) { + this.idToken = setterArg; + return this; + } + + private @Nullable String serverAuthCode; + + public @NonNull Builder setServerAuthCode(@Nullable String setterArg) { + this.serverAuthCode = setterArg; + return this; + } + + public @NonNull UserData build() { + UserData pigeonReturn = new UserData(); + pigeonReturn.setDisplayName(displayName); + pigeonReturn.setEmail(email); + pigeonReturn.setId(id); + pigeonReturn.setPhotoUrl(photoUrl); + pigeonReturn.setIdToken(idToken); + pigeonReturn.setServerAuthCode(serverAuthCode); + return pigeonReturn; + } + } + + @NonNull + ArrayList toList() { + ArrayList toListResult = new ArrayList(6); + toListResult.add(displayName); + toListResult.add(email); + toListResult.add(id); + toListResult.add(photoUrl); + toListResult.add(idToken); + toListResult.add(serverAuthCode); + return toListResult; + } + + static @NonNull UserData fromList(@NonNull ArrayList list) { + UserData pigeonResult = new UserData(); + Object displayName = list.get(0); + pigeonResult.setDisplayName((String) displayName); + Object email = list.get(1); + pigeonResult.setEmail((String) email); + Object id = list.get(2); + pigeonResult.setId((String) id); + Object photoUrl = list.get(3); + pigeonResult.setPhotoUrl((String) photoUrl); + Object idToken = list.get(4); + pigeonResult.setIdToken((String) idToken); + Object serverAuthCode = list.get(5); + pigeonResult.setServerAuthCode((String) serverAuthCode); + return pigeonResult; + } + } + + public interface Result { + @SuppressWarnings("UnknownNullness") + void success(T result); + + void error(@NonNull Throwable error); + } + + private static class GoogleSignInApiCodec extends StandardMessageCodec { + public static final GoogleSignInApiCodec INSTANCE = new GoogleSignInApiCodec(); + + private GoogleSignInApiCodec() {} + + @Override + protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) { + switch (type) { + case (byte) 128: + return InitParams.fromList((ArrayList) readValue(buffer)); + case (byte) 129: + return UserData.fromList((ArrayList) readValue(buffer)); + default: + return super.readValueOfType(type, buffer); + } + } + + @Override + protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) { + if (value instanceof InitParams) { + stream.write(128); + writeValue(stream, ((InitParams) value).toList()); + } else if (value instanceof UserData) { + stream.write(129); + writeValue(stream, ((UserData) value).toList()); + } else { + super.writeValue(stream, value); + } + } + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface GoogleSignInApi { + /** Initializes a sign in request with the given parameters. */ + void init(@NonNull InitParams params); + /** Starts a silent sign in. */ + void signInSilently(@NonNull Result result); + /** Starts a sign in with user interaction. */ + void signIn(@NonNull Result result); + /** Requests the access token for the current sign in. */ + void getAccessToken( + @NonNull String email, @NonNull Boolean shouldRecoverAuth, @NonNull Result result); + /** Signs out the current user. */ + void signOut(@NonNull Result result); + /** Revokes scope grants to the application. */ + void disconnect(@NonNull Result result); + /** Returns whether the user is currently signed in. */ + @NonNull + Boolean isSignedIn(); + /** Clears the authentication caching for the given token, requiring a new sign in. */ + void clearAuthCache(@NonNull String token, @NonNull Result result); + /** Requests access to the given scopes. */ + void requestScopes(@NonNull List scopes, @NonNull Result result); + + /** The codec used by GoogleSignInApi. */ + static @NonNull MessageCodec getCodec() { + return GoogleSignInApiCodec.INSTANCE; + } + /** + * Sets up an instance of `GoogleSignInApi` to handle messages through the `binaryMessenger`. + */ + static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable GoogleSignInApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.init", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + InitParams paramsArg = (InitParams) args.get(0); + try { + api.init(paramsArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signInSilently", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(UserData result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signInSilently(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signIn", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(UserData result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signIn(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.getAccessToken", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String emailArg = (String) args.get(0); + Boolean shouldRecoverAuthArg = (Boolean) args.get(1); + Result resultCallback = + new Result() { + public void success(String result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getAccessToken(emailArg, shouldRecoverAuthArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signOut", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signOut(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.disconnect", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.disconnect(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.isSignedIn", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + try { + Boolean output = api.isSignedIn(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.clearAuthCache", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String tokenArg = (String) args.get(0); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.clearAuthCache(tokenArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.requestScopes", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + List scopesArg = (List) args.get(0); + Result resultCallback = + new Result() { + public void success(Boolean result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.requestScopes(scopesArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } +} diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java new file mode 100644 index 0000000000..4d8c79e22a --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInLegacyMethodChannelTest.java @@ -0,0 +1,378 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesignin; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; +import com.google.android.gms.auth.api.signin.GoogleSignInOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.CommonStatusCodes; +import com.google.android.gms.common.api.Scope; +import com.google.android.gms.common.api.Status; +import com.google.android.gms.tasks.Task; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class GoogleSignInLegacyMethodChannelTest { + @Mock Context mockContext; + @Mock Resources mockResources; + @Mock Activity mockActivity; + @Mock BinaryMessenger mockMessenger; + @Spy MethodChannel.Result result; + @Mock GoogleSignInWrapper mockGoogleSignIn; + @Mock GoogleSignInAccount account; + @Mock GoogleSignInClient mockClient; + @Mock Task mockSignInTask; + + @SuppressWarnings("deprecation") + @Mock + PluginRegistry.Registrar mockRegistrar; + + private GoogleSignInPlugin plugin; + private AutoCloseable mockCloseable; + + @Before + public void setUp() { + mockCloseable = MockitoAnnotations.openMocks(this); + when(mockRegistrar.messenger()).thenReturn(mockMessenger); + when(mockRegistrar.context()).thenReturn(mockContext); + when(mockRegistrar.activity()).thenReturn(mockActivity); + when(mockContext.getResources()).thenReturn(mockResources); + plugin = new GoogleSignInPlugin(); + plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); + plugin.setUpRegistrar(mockRegistrar); + } + + @After + public void tearDown() throws Exception { + mockCloseable.close(); + } + + @Test + public void requestScopes_ResultErrorIfAccountIsNull() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + plugin.onMethodCall(methodCall, result); + verify(result).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test + public void requestScopes_ResultTrueIfAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); + + plugin.onMethodCall(methodCall, result); + verify(result).success(true); + } + + @Test + public void requestScopes_RequestsPermissionIfNotGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + + verify(mockGoogleSignIn) + .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + } + + @Test + public void requestScopes_ReturnsFalseIfPermissionDenied() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, + Activity.RESULT_CANCELED, + new Intent()); + + verify(result).success(false); + } + + @Test + public void requestScopes_ReturnsTrueIfPermissionGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test(expected = IllegalStateException.class) + public void signInThrowsWithoutActivity() { + final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); + plugin.initInstance( + mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class)); + + plugin.onMethodCall(new MethodCall("signIn", null), null); + } + + @Test + public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError() + throws ApiException { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + + ApiException exception = + new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text")); + when(mockClient.silentSignIn()).thenReturn(mockSignInTask); + when(mockSignInTask.isComplete()).thenReturn(true); + when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception); + + plugin.onMethodCall(new MethodCall("signInSilently", null), result); + verify(result) + .error( + "sign_in_required", + "com.google.android.gms.common.api.ApiException: 4: Error text", + null); + } + + @Test + public void init_LoadsServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_InterpretsClientIdAsServerClientId() { + final String clientId = "fakeClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, null); + initAndAssertServerClientId(methodCall, clientId); + } + + @Test + public void init_ForwardsServerClientId() { + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(null, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_IgnoresClientIdIfServerClientIdIsProvided() { + final String clientId = "fakeClientId"; + final String serverClientId = "fakeServerClientId"; + MethodCall methodCall = buildInitMethodCall(clientId, serverClientId); + initAndAssertServerClientId(methodCall, serverClientId); + } + + @Test + public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() { + MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", false); + + initAndAssertForceCodeForRefreshToken(methodCall, false); + } + + @Test + public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() { + MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", true); + + initAndAssertForceCodeForRefreshToken(methodCall, true); + } + + @Test + public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null, false); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + + initAndAssertForceCodeForRefreshToken(methodCall, false); + } + + @Test + public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdFromResources() { + final String packageName = "fakePackageName"; + final String serverClientId = "fakeServerClientId"; + final int resourceId = 1; + MethodCall methodCall = buildInitMethodCall(null, null, true); + when(mockContext.getPackageName()).thenReturn(packageName); + when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) + .thenReturn(resourceId); + when(mockContext.getString(resourceId)).thenReturn(serverClientId); + + initAndAssertForceCodeForRefreshToken(methodCall, true); + } + + public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) { + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(GoogleSignInOptions.class); + when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) + .thenReturn(mockClient); + plugin.onMethodCall(methodCall, result); + verify(result).success(null); + Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId()); + } + + public void initAndAssertForceCodeForRefreshToken( + MethodCall methodCall, boolean forceCodeForRefreshToken) { + ArgumentCaptor optionsCaptor = + ArgumentCaptor.forClass(GoogleSignInOptions.class); + when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) + .thenReturn(mockClient); + plugin.onMethodCall(methodCall, result); + verify(result).success(null); + Assert.assertEquals( + forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken()); + } + + private static MethodCall buildInitMethodCall(String clientId, String serverClientId) { + return buildInitMethodCall( + "SignInOption.standard", Collections.emptyList(), clientId, serverClientId, false); + } + + private static MethodCall buildInitMethodCall( + String clientId, String serverClientId, boolean forceCodeForRefreshToken) { + return buildInitMethodCall( + "SignInOption.standard", + Collections.emptyList(), + clientId, + serverClientId, + forceCodeForRefreshToken); + } + + private static MethodCall buildInitMethodCall( + String signInOption, + List scopes, + String clientId, + String serverClientId, + boolean forceCodeForRefreshToken) { + HashMap arguments = new HashMap<>(); + arguments.put("signInOption", signInOption); + arguments.put("scopes", scopes); + if (clientId != null) { + arguments.put("clientId", clientId); + } + if (serverClientId != null) { + arguments.put("serverClientId", serverClientId); + } + arguments.put("forceCodeForRefreshToken", forceCodeForRefreshToken); + return new MethodCall("init", arguments); + } +} diff --git a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java index 627ac40a48..b317d8042f 100644 --- a/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java +++ b/packages/google_sign_in/google_sign_in_android/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -23,11 +23,10 @@ import com.google.android.gms.common.api.Scope; import com.google.android.gms.common.api.Status; import com.google.android.gms.tasks.Task; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugins.googlesignin.Messages.FlutterError; +import io.flutter.plugins.googlesignin.Messages.InitParams; import java.util.Collections; -import java.util.HashMap; import java.util.List; import org.junit.After; import org.junit.Assert; @@ -43,7 +42,9 @@ public class GoogleSignInTest { @Mock Resources mockResources; @Mock Activity mockActivity; @Mock BinaryMessenger mockMessenger; - @Spy MethodChannel.Result result; + @Spy Messages.Result voidResult; + @Spy Messages.Result boolResult; + @Spy Messages.Result userDataResult; @Mock GoogleSignInWrapper mockGoogleSignIn; @Mock GoogleSignInAccount account; @Mock GoogleSignInClient mockClient; @@ -53,7 +54,7 @@ public class GoogleSignInTest { @Mock PluginRegistry.Registrar mockRegistrar; - private GoogleSignInPlugin plugin; + private GoogleSignInPlugin.Delegate plugin; private AutoCloseable mockCloseable; @Before @@ -63,8 +64,7 @@ public class GoogleSignInTest { when(mockRegistrar.context()).thenReturn(mockContext); when(mockRegistrar.activity()).thenReturn(mockActivity); when(mockContext.getResources()).thenReturn(mockResources); - plugin = new GoogleSignInPlugin(); - plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); + plugin = new GoogleSignInPlugin.Delegate(mockRegistrar.context(), mockGoogleSignIn); plugin.setUpRegistrar(mockRegistrar); } @@ -75,41 +75,37 @@ public class GoogleSignInTest { @Test public void requestScopes_ResultErrorIfAccountIsNull() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - plugin.onMethodCall(methodCall, result); - verify(result).error("sign_in_required", "No account to grant scopes.", null); + + plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); + + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(boolResult).error(resultCaptor.capture()); + FlutterError error = (FlutterError) resultCaptor.getValue(); + Assert.assertEquals("sign_in_required", error.code); + Assert.assertEquals("No account to grant scopes.", error.getMessage()); } @Test public void requestScopes_ResultTrueIfAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); - plugin.onMethodCall(methodCall, result); - verify(result).success(true); + plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); + + verify(boolResult).success(true); } @Test public void requestScopes_RequestsPermissionIfNotGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); verify(mockGoogleSignIn) .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); @@ -117,11 +113,7 @@ public class GoogleSignInTest { @Test public void requestScopes_ReturnsFalseIfPermissionDenied() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor captor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockRegistrar).addActivityResultListener(captor.capture()); @@ -131,22 +123,18 @@ public class GoogleSignInTest { when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_CANCELED, new Intent()); - verify(result).success(false); + verify(boolResult).success(false); } @Test public void requestScopes_ReturnsTrueIfPermissionGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor captor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockRegistrar).addActivityResultListener(captor.capture()); @@ -156,20 +144,17 @@ public class GoogleSignInTest { when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(Collections.singletonList("requestedScope"), boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - verify(result).success(true); + verify(boolResult).success(true); } @Test public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); + List requestedScopes = Collections.singletonList("requestedScope"); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor captor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockRegistrar).addActivityResultListener(captor.capture()); @@ -179,23 +164,19 @@ public class GoogleSignInTest { when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(requestedScopes, boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(requestedScopes, boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - verify(result, times(2)).success(true); + verify(boolResult, times(2)).success(true); } @Test public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - + List requestedScopes = Collections.singletonList("requestedScope"); ArgumentCaptor captor = ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); verify(mockRegistrar).addActivityResultListener(captor.capture()); @@ -203,31 +184,39 @@ public class GoogleSignInTest { when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(requestedScopes, boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); + plugin.requestScopes(requestedScopes, boolResult); listener.onActivityResult( GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(boolResult, times(2)).error(resultCaptor.capture()); + List errors = resultCaptor.getAllValues(); + Assert.assertEquals(2, errors.size()); + FlutterError error = (FlutterError) errors.get(0); + Assert.assertEquals("sign_in_required", error.code); + Assert.assertEquals("No account to grant scopes.", error.getMessage()); + error = (FlutterError) errors.get(1); + Assert.assertEquals("sign_in_required", error.code); + Assert.assertEquals("No account to grant scopes.", error.getMessage()); } @Test(expected = IllegalStateException.class) public void signInThrowsWithoutActivity() { - final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); - plugin.initInstance( - mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class)); + final GoogleSignInPlugin.Delegate plugin = + new GoogleSignInPlugin.Delegate(mock(Context.class), mock(GoogleSignInWrapper.class)); - plugin.onMethodCall(new MethodCall("signIn", null), null); + plugin.signIn(userDataResult); } @Test public void signInSilentlyThatImmediatelyCompletesWithoutResultFinishesWithError() throws ApiException { final String clientId = "fakeClientId"; - MethodCall methodCall = buildInitMethodCall(clientId, null); - initAndAssertServerClientId(methodCall, clientId); + InitParams params = buildInitParams(clientId, null); + initAndAssertServerClientId(params, clientId); ApiException exception = new ApiException(new Status(CommonStatusCodes.SIGN_IN_REQUIRED, "Error text")); @@ -235,12 +224,13 @@ public class GoogleSignInTest { when(mockSignInTask.isComplete()).thenReturn(true); when(mockSignInTask.getResult(ApiException.class)).thenThrow(exception); - plugin.onMethodCall(new MethodCall("signInSilently", null), result); - verify(result) - .error( - "sign_in_required", - "com.google.android.gms.common.api.ApiException: 4: Error text", - null); + plugin.signInSilently(userDataResult); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(userDataResult).error(resultCaptor.capture()); + FlutterError error = (FlutterError) resultCaptor.getValue(); + Assert.assertEquals("sign_in_required", error.code); + Assert.assertEquals( + "com.google.android.gms.common.api.ApiException: 4: Error text", error.getMessage()); } @Test @@ -248,48 +238,48 @@ public class GoogleSignInTest { final String packageName = "fakePackageName"; final String serverClientId = "fakeServerClientId"; final int resourceId = 1; - MethodCall methodCall = buildInitMethodCall(null, null); + InitParams params = buildInitParams(null, null); when(mockContext.getPackageName()).thenReturn(packageName); when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) .thenReturn(resourceId); when(mockContext.getString(resourceId)).thenReturn(serverClientId); - initAndAssertServerClientId(methodCall, serverClientId); + initAndAssertServerClientId(params, serverClientId); } @Test public void init_InterpretsClientIdAsServerClientId() { final String clientId = "fakeClientId"; - MethodCall methodCall = buildInitMethodCall(clientId, null); - initAndAssertServerClientId(methodCall, clientId); + InitParams params = buildInitParams(clientId, null); + initAndAssertServerClientId(params, clientId); } @Test public void init_ForwardsServerClientId() { final String serverClientId = "fakeServerClientId"; - MethodCall methodCall = buildInitMethodCall(null, serverClientId); - initAndAssertServerClientId(methodCall, serverClientId); + InitParams params = buildInitParams(null, serverClientId); + initAndAssertServerClientId(params, serverClientId); } @Test public void init_IgnoresClientIdIfServerClientIdIsProvided() { final String clientId = "fakeClientId"; final String serverClientId = "fakeServerClientId"; - MethodCall methodCall = buildInitMethodCall(clientId, serverClientId); - initAndAssertServerClientId(methodCall, serverClientId); + InitParams params = buildInitParams(clientId, serverClientId); + initAndAssertServerClientId(params, serverClientId); } @Test public void init_PassesForceCodeForRefreshTokenFalseWithServerClientIdParameter() { - MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", false); + InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", false); - initAndAssertForceCodeForRefreshToken(methodCall, false); + initAndAssertForceCodeForRefreshToken(params, false); } @Test public void init_PassesForceCodeForRefreshTokenTrueWithServerClientIdParameter() { - MethodCall methodCall = buildInitMethodCall("fakeClientId", "fakeServerClientId", true); + InitParams params = buildInitParams("fakeClientId", "fakeServerClientId", true); - initAndAssertForceCodeForRefreshToken(methodCall, true); + initAndAssertForceCodeForRefreshToken(params, true); } @Test @@ -297,13 +287,13 @@ public class GoogleSignInTest { final String packageName = "fakePackageName"; final String serverClientId = "fakeServerClientId"; final int resourceId = 1; - MethodCall methodCall = buildInitMethodCall(null, null, false); + InitParams params = buildInitParams(null, null, false); when(mockContext.getPackageName()).thenReturn(packageName); when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) .thenReturn(resourceId); when(mockContext.getString(resourceId)).thenReturn(serverClientId); - initAndAssertForceCodeForRefreshToken(methodCall, false); + initAndAssertForceCodeForRefreshToken(params, false); } @Test @@ -311,68 +301,66 @@ public class GoogleSignInTest { final String packageName = "fakePackageName"; final String serverClientId = "fakeServerClientId"; final int resourceId = 1; - MethodCall methodCall = buildInitMethodCall(null, null, true); + InitParams params = buildInitParams(null, null, true); when(mockContext.getPackageName()).thenReturn(packageName); when(mockResources.getIdentifier("default_web_client_id", "string", packageName)) .thenReturn(resourceId); when(mockContext.getString(resourceId)).thenReturn(serverClientId); - initAndAssertForceCodeForRefreshToken(methodCall, true); + initAndAssertForceCodeForRefreshToken(params, true); } - public void initAndAssertServerClientId(MethodCall methodCall, String serverClientId) { + public void initAndAssertServerClientId(InitParams params, String serverClientId) { ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(GoogleSignInOptions.class); when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) .thenReturn(mockClient); - plugin.onMethodCall(methodCall, result); - verify(result).success(null); + plugin.init(params); Assert.assertEquals(serverClientId, optionsCaptor.getValue().getServerClientId()); } public void initAndAssertForceCodeForRefreshToken( - MethodCall methodCall, boolean forceCodeForRefreshToken) { + InitParams params, boolean forceCodeForRefreshToken) { ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(GoogleSignInOptions.class); when(mockGoogleSignIn.getClient(any(Context.class), optionsCaptor.capture())) .thenReturn(mockClient); - plugin.onMethodCall(methodCall, result); - verify(result).success(null); + plugin.init(params); Assert.assertEquals( forceCodeForRefreshToken, optionsCaptor.getValue().isForceCodeForRefreshToken()); } - private static MethodCall buildInitMethodCall(String clientId, String serverClientId) { - return buildInitMethodCall( - "SignInOption.standard", Collections.emptyList(), clientId, serverClientId, false); + private static InitParams buildInitParams(String clientId, String serverClientId) { + return buildInitParams( + Messages.SignInType.STANDARD, Collections.emptyList(), clientId, serverClientId, false); } - private static MethodCall buildInitMethodCall( + private static InitParams buildInitParams( String clientId, String serverClientId, boolean forceCodeForRefreshToken) { - return buildInitMethodCall( - "SignInOption.standard", - Collections.emptyList(), + return buildInitParams( + Messages.SignInType.STANDARD, + Collections.emptyList(), clientId, serverClientId, forceCodeForRefreshToken); } - private static MethodCall buildInitMethodCall( - String signInOption, + private static InitParams buildInitParams( + Messages.SignInType signInType, List scopes, String clientId, String serverClientId, boolean forceCodeForRefreshToken) { - HashMap arguments = new HashMap<>(); - arguments.put("signInOption", signInOption); - arguments.put("scopes", scopes); + InitParams.Builder builder = new InitParams.Builder(); + builder.setSignInType(signInType); + builder.setScopes(scopes); if (clientId != null) { - arguments.put("clientId", clientId); + builder.setClientId(clientId); } if (serverClientId != null) { - arguments.put("serverClientId", serverClientId); + builder.setServerClientId(serverClientId); } - arguments.put("forceCodeForRefreshToken", forceCodeForRefreshToken); - return new MethodCall("init", arguments); + builder.setForceCodeForRefreshToken(forceCodeForRefreshToken); + return builder.build(); } } diff --git a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart index ce651d7e8d..48f4abb75c 100644 --- a/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart +++ b/packages/google_sign_in/google_sign_in_android/lib/google_sign_in_android.dart @@ -5,18 +5,18 @@ import 'dart:async'; import 'package:flutter/foundation.dart' show visibleForTesting; -import 'package:flutter/services.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'src/utils.dart'; +import 'src/messages.g.dart'; /// Android implementation of [GoogleSignInPlatform]. class GoogleSignInAndroid extends GoogleSignInPlatform { - /// This is only exposed for test purposes. It shouldn't be used by clients of - /// the plugin as it may break or change at any time. - @visibleForTesting - MethodChannel channel = - const MethodChannel('plugins.flutter.io/google_sign_in_android'); + /// Creates a new plugin implementation instance. + GoogleSignInAndroid({ + @visibleForTesting GoogleSignInApi? api, + }) : _api = api ?? GoogleSignInApi(); + + final GoogleSignInApi _api; /// Registers this class as the default instance of [GoogleSignInPlatform]. static void registerWith() { @@ -40,68 +40,84 @@ class GoogleSignInAndroid extends GoogleSignInPlatform { @override Future initWithParams(SignInInitParameters params) { - return channel.invokeMethod('init', { - 'signInOption': params.signInOption.toString(), - 'scopes': params.scopes, - 'hostedDomain': params.hostedDomain, - 'clientId': params.clientId, - 'serverClientId': params.serverClientId, - 'forceCodeForRefreshToken': params.forceCodeForRefreshToken, - }); + return _api.init(InitParams( + signInType: _signInTypeForOption(params.signInOption), + scopes: params.scopes, + hostedDomain: params.hostedDomain, + clientId: params.clientId, + serverClientId: params.serverClientId, + forceCodeForRefreshToken: params.forceCodeForRefreshToken, + )); } @override Future signInSilently() { - return channel - .invokeMapMethod('signInSilently') - .then(getUserDataFromMap); + return _api.signInSilently().then(_signInUserDataFromChannelData); } @override Future signIn() { - return channel - .invokeMapMethod('signIn') - .then(getUserDataFromMap); + return _api.signIn().then(_signInUserDataFromChannelData); } @override Future getTokens( {required String email, bool? shouldRecoverAuth = true}) { - return channel - .invokeMapMethod('getTokens', { - 'email': email, - 'shouldRecoverAuth': shouldRecoverAuth ?? true, - }).then((Map? result) => getTokenDataFromMap(result!)); + return _api + .getAccessToken(email, shouldRecoverAuth ?? true) + .then((String result) => GoogleSignInTokenData( + accessToken: result, + )); } @override Future signOut() { - return channel.invokeMapMethod('signOut'); + return _api.signOut(); } @override Future disconnect() { - return channel.invokeMapMethod('disconnect'); + return _api.disconnect(); } @override - Future isSignedIn() async { - return (await channel.invokeMethod('isSignedIn'))!; + Future isSignedIn() { + return _api.isSignedIn(); } @override Future clearAuthCache({String? token}) { - return channel.invokeMethod( - 'clearAuthCache', - {'token': token}, - ); + // The token is not acutally nullable; see + // https://github.com/flutter/flutter/issues/129717 + return _api.clearAuthCache(token!); } @override - Future requestScopes(List scopes) async { - return (await channel.invokeMethod( - 'requestScopes', - >{'scopes': scopes}, - ))!; + Future requestScopes(List scopes) { + return _api.requestScopes(scopes); + } + + SignInType _signInTypeForOption(SignInOption option) { + switch (option) { + case SignInOption.standard: + return SignInType.standard; + case SignInOption.games: + return SignInType.games; + } + // Handle the case where a new type is added to the platform interface in + // the future, and this version of the package is used with it. + // ignore: dead_code + throw UnimplementedError('Unsupported sign in option: $option'); + } + + GoogleSignInUserData _signInUserDataFromChannelData(UserData data) { + return GoogleSignInUserData( + email: data.email, + id: data.id, + displayName: data.displayName, + photoUrl: data.photoUrl, + idToken: data.idToken, + serverAuthCode: data.serverAuthCode, + ); } } diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart new file mode 100644 index 0000000000..14d8cb5fdc --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/lib/src/messages.g.dart @@ -0,0 +1,387 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v10.1.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import + +import 'dart:async'; +import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; + +import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; +import 'package:flutter/services.dart'; + +/// Pigeon version of SignInOption. +enum SignInType { + /// Default configuration. + standard, + + /// Recommended configuration for game sign in. + games, +} + +/// Pigeon version of SignInInitParams. +/// +/// See SignInInitParams for details. +class InitParams { + InitParams({ + required this.scopes, + required this.signInType, + this.hostedDomain, + this.clientId, + this.serverClientId, + required this.forceCodeForRefreshToken, + }); + + List scopes; + + SignInType signInType; + + String? hostedDomain; + + String? clientId; + + String? serverClientId; + + bool forceCodeForRefreshToken; + + Object encode() { + return [ + scopes, + signInType.index, + hostedDomain, + clientId, + serverClientId, + forceCodeForRefreshToken, + ]; + } + + static InitParams decode(Object result) { + result as List; + return InitParams( + scopes: (result[0] as List?)!.cast(), + signInType: SignInType.values[result[1]! as int], + hostedDomain: result[2] as String?, + clientId: result[3] as String?, + serverClientId: result[4] as String?, + forceCodeForRefreshToken: result[5]! as bool, + ); + } +} + +/// Pigeon version of GoogleSignInUserData. +/// +/// See GoogleSignInUserData for details. +class UserData { + UserData({ + this.displayName, + required this.email, + required this.id, + this.photoUrl, + this.idToken, + this.serverAuthCode, + }); + + String? displayName; + + String email; + + String id; + + String? photoUrl; + + String? idToken; + + String? serverAuthCode; + + Object encode() { + return [ + displayName, + email, + id, + photoUrl, + idToken, + serverAuthCode, + ]; + } + + static UserData decode(Object result) { + result as List; + return UserData( + displayName: result[0] as String?, + email: result[1]! as String, + id: result[2]! as String, + photoUrl: result[3] as String?, + idToken: result[4] as String?, + serverAuthCode: result[5] as String?, + ); + } +} + +class _GoogleSignInApiCodec extends StandardMessageCodec { + const _GoogleSignInApiCodec(); + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is InitParams) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else if (value is UserData) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return InitParams.decode(readValue(buffer)!); + case 129: + return UserData.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +class GoogleSignInApi { + /// Constructor for [GoogleSignInApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GoogleSignInApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = _GoogleSignInApiCodec(); + + /// Initializes a sign in request with the given parameters. + Future init(InitParams arg_params) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.init', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_params]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Starts a silent sign in. + Future signInSilently() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.signInSilently', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as UserData?)!; + } + } + + /// Starts a sign in with user interaction. + Future signIn() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.signIn', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as UserData?)!; + } + } + + /// Requests the access token for the current sign in. + Future getAccessToken( + String arg_email, bool arg_shouldRecoverAuth) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.getAccessToken', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_email, arg_shouldRecoverAuth]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as String?)!; + } + } + + /// Signs out the current user. + Future signOut() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.signOut', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Revokes scope grants to the application. + Future disconnect() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.disconnect', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Returns whether the user is currently signed in. + Future isSignedIn() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.isSignedIn', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel.send(null) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } + + /// Clears the authentication caching for the given token, requiring a + /// new sign in. + Future clearAuthCache(String arg_token) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.clearAuthCache', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_token]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } + + /// Requests access to the given scopes. + Future requestScopes(List arg_scopes) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.GoogleSignInApi.requestScopes', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_scopes]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as bool?)!; + } + } +} diff --git a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart deleted file mode 100644 index 5cd7c20b82..0000000000 --- a/packages/google_sign_in/google_sign_in_android/lib/src/utils.dart +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; - -/// Converts user data coming from native code into the proper platform interface type. -GoogleSignInUserData? getUserDataFromMap(Map? data) { - if (data == null) { - return null; - } - return GoogleSignInUserData( - email: data['email']! as String, - id: data['id']! as String, - displayName: data['displayName'] as String?, - photoUrl: data['photoUrl'] as String?, - idToken: data['idToken'] as String?, - serverAuthCode: data['serverAuthCode'] as String?); -} - -/// Converts token data coming from native code into the proper platform interface type. -GoogleSignInTokenData getTokenDataFromMap(Map data) { - return GoogleSignInTokenData( - idToken: data['idToken'] as String?, - accessToken: data['accessToken'] as String?, - serverAuthCode: data['serverAuthCode'] as String?, - ); -} diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt b/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt new file mode 100644 index 0000000000..1236b63caf --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/pigeons/copyright.txt @@ -0,0 +1,3 @@ +Copyright 2013 The Flutter Authors. All rights reserved. +Use of this source code is governed by a BSD-style license that can be +found in the LICENSE file. diff --git a/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart new file mode 100644 index 0000000000..4804d43eac --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/pigeons/messages.dart @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:pigeon/pigeon.dart'; + +@ConfigurePigeon(PigeonOptions( + dartOut: 'lib/src/messages.g.dart', + javaOut: + 'android/src/main/java/io/flutter/plugins/googlesignin/Messages.java', + javaOptions: JavaOptions(package: 'io.flutter.plugins.googlesignin'), + copyrightHeader: 'pigeons/copyright.txt', +)) + +/// Pigeon version of SignInOption. +enum SignInType { + /// Default configuration. + standard, + + /// Recommended configuration for game sign in. + games, +} + +/// Pigeon version of SignInInitParams. +/// +/// See SignInInitParams for details. +class InitParams { + /// The parameters to use when initializing the sign in process. + const InitParams({ + this.scopes = const [], + this.signInType = SignInType.standard, + this.hostedDomain, + this.clientId, + this.serverClientId, + this.forceCodeForRefreshToken = false, + }); + + // TODO(stuartmorgan): Make the generic type non-nullable once supported. + // https://github.com/flutter/flutter/issues/97848 + // The Java code treats the values as non-nullable. + final List scopes; + final SignInType signInType; + final String? hostedDomain; + final String? clientId; + final String? serverClientId; + final bool forceCodeForRefreshToken; +} + +/// Pigeon version of GoogleSignInUserData. +/// +/// See GoogleSignInUserData for details. +class UserData { + UserData({ + required this.email, + required this.id, + this.displayName, + this.photoUrl, + this.idToken, + this.serverAuthCode, + }); + + final String? displayName; + final String email; + final String id; + final String? photoUrl; + final String? idToken; + final String? serverAuthCode; +} + +@HostApi() +abstract class GoogleSignInApi { + /// Initializes a sign in request with the given parameters. + void init(InitParams params); + + /// Starts a silent sign in. + @async + UserData signInSilently(); + + /// Starts a sign in with user interaction. + @async + UserData signIn(); + + /// Requests the access token for the current sign in. + @async + String getAccessToken(String email, bool shouldRecoverAuth); + + /// Signs out the current user. + @async + void signOut(); + + /// Revokes scope grants to the application. + @async + void disconnect(); + + /// Returns whether the user is currently signed in. + bool isSignedIn(); + + /// Clears the authentication caching for the given token, requiring a + /// new sign in. + @async + void clearAuthCache(String token); + + /// Requests access to the given scopes. + @async + bool requestScopes(List scopes); +} diff --git a/packages/google_sign_in/google_sign_in_android/pubspec.yaml b/packages/google_sign_in/google_sign_in_android/pubspec.yaml index a109d41de9..58bd307388 100644 --- a/packages/google_sign_in/google_sign_in_android/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_android/pubspec.yaml @@ -2,7 +2,7 @@ name: google_sign_in_android description: Android implementation of the google_sign_in plugin. repository: https://github.com/flutter/packages/tree/main/packages/google_sign_in/google_sign_in_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+google_sign_in%22 -version: 6.1.16 +version: 6.1.17 environment: sdk: ">=2.18.0 <4.0.0" @@ -23,10 +23,13 @@ dependencies: google_sign_in_platform_interface: ^2.2.0 dev_dependencies: + build_runner: ^2.3.0 flutter_test: sdk: flutter integration_test: sdk: flutter + mockito: 5.4.1 + pigeon: ^10.1.0 # The example deliberately includes limited-use secrets. false_secrets: diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart index 671d7683b2..e53b9a8f54 100644 --- a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.dart @@ -5,65 +5,35 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_sign_in_android/google_sign_in_android.dart'; -import 'package:google_sign_in_android/src/utils.dart'; +import 'package:google_sign_in_android/src/messages.g.dart'; import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; -const Map kUserData = { - 'email': 'john.doe@gmail.com', - 'id': '8162538176523816253123', - 'photoUrl': 'https://lh5.googleusercontent.com/photo.jpg', - 'displayName': 'John Doe', - 'idToken': '123', - 'serverAuthCode': '789', -}; +import 'google_sign_in_android_test.mocks.dart'; -const Map kTokenData = { - 'idToken': '123', - 'accessToken': '456', - 'serverAuthCode': '789', -}; - -const Map kDefaultResponses = { - 'init': null, - 'signInSilently': kUserData, - 'signIn': kUserData, - 'signOut': null, - 'disconnect': null, - 'isSignedIn': true, - 'getTokens': kTokenData, - 'requestScopes': true, -}; - -final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData kToken = - getTokenDataFromMap(kTokenData as Map); +final GoogleSignInUserData _user = GoogleSignInUserData( + email: 'john.doe@gmail.com', + id: '8162538176523816253123', + photoUrl: 'https://lh5.googleusercontent.com/photo.jpg', + displayName: 'John Doe', + idToken: '123', + serverAuthCode: '789', +); +final GoogleSignInTokenData _token = GoogleSignInTokenData( + accessToken: '456', +); +@GenerateMocks([GoogleSignInApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final GoogleSignInAndroid googleSignIn = GoogleSignInAndroid(); - final MethodChannel channel = googleSignIn.channel; - - final List log = []; - late Map - responses; // Some tests mutate some kDefaultResponses + late GoogleSignInAndroid googleSignIn; + late MockGoogleSignInApi api; setUp(() { - responses = Map.from(kDefaultResponses); - _ambiguate(TestDefaultBinaryMessengerBinding.instance)! - .defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (MethodCall methodCall) { - log.add(methodCall); - final dynamic response = responses[methodCall.method]; - if (response != null && response is Exception) { - return Future.error('$response'); - } - return Future.value(response); - }, - ); - log.clear(); + api = MockGoogleSignInApi(); + googleSignIn = GoogleSignInAndroid(api: api); }); test('registered instance', () { @@ -73,111 +43,159 @@ void main() { test('signInSilently transforms platform data to GoogleSignInUserData', () async { + when(api.signInSilently()).thenAnswer((_) async => UserData( + email: _user.email, + id: _user.id, + photoUrl: _user.photoUrl, + displayName: _user.displayName, + idToken: _user.idToken, + serverAuthCode: _user.serverAuthCode, + )); + final dynamic response = await googleSignIn.signInSilently(); - expect(response, kUser); + + expect(response, _user); }); + test('signInSilently Exceptions -> throws', () async { - responses['signInSilently'] = Exception('Not a user'); + when(api.signInSilently()) + .thenAnswer((_) async => throw PlatformException(code: 'fail')); + expect(googleSignIn.signInSilently(), throwsA(isInstanceOf())); }); test('signIn transforms platform data to GoogleSignInUserData', () async { + when(api.signIn()).thenAnswer((_) async => UserData( + email: _user.email, + id: _user.id, + photoUrl: _user.photoUrl, + displayName: _user.displayName, + idToken: _user.idToken, + serverAuthCode: _user.serverAuthCode, + )); + final dynamic response = await googleSignIn.signIn(); - expect(response, kUser); + + expect(response, _user); }); + test('signIn Exceptions -> throws', () async { - responses['signIn'] = Exception('Not a user'); + when(api.signIn()) + .thenAnswer((_) async => throw PlatformException(code: 'fail')); + expect(googleSignIn.signIn(), throwsA(isInstanceOf())); }); test('getTokens transforms platform data to GoogleSignInTokenData', () async { - final dynamic response = await googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: false); - expect(response, kToken); - expect( - log[0], - isMethodCall('getTokens', arguments: { - 'email': 'example@example.com', - 'shouldRecoverAuth': false, - })); + const bool recoverAuth = false; + when(api.getAccessToken(_user.email, recoverAuth)) + .thenAnswer((_) async => _token.accessToken!); + + final GoogleSignInTokenData response = await googleSignIn.getTokens( + email: _user.email, shouldRecoverAuth: recoverAuth); + + expect(response, _token); }); test('getTokens will not pass null for shouldRecoverAuth', () async { - await googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: null); - expect( - log[0], - isMethodCall('getTokens', arguments: { - 'email': 'example@example.com', - 'shouldRecoverAuth': true, - })); + when(api.getAccessToken(_user.email, true)) + .thenAnswer((_) async => _token.accessToken!); + + final GoogleSignInTokenData response = await googleSignIn.getTokens( + email: _user.email, shouldRecoverAuth: null); + + expect(response, _token); }); - test('Other functions pass through arguments to the channel', () async { - final Map tests = { - () { - googleSignIn.init( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId'); - }: isMethodCall('init', arguments: { - 'hostedDomain': 'example.com', - 'scopes': ['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': null, - 'forceCodeForRefreshToken': false, - }), - () { - googleSignIn.initWithParams(const SignInInitParameters( - hostedDomain: 'example.com', - scopes: ['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId', - serverClientId: 'fakeServerClientId', - forceCodeForRefreshToken: true)); - }: isMethodCall('init', arguments: { - 'hostedDomain': 'example.com', - 'scopes': ['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': 'fakeServerClientId', - 'forceCodeForRefreshToken': true, - }), - () { - googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: false); - }: isMethodCall('getTokens', arguments: { - 'email': 'example@example.com', - 'shouldRecoverAuth': false, - }), - () { - googleSignIn.clearAuthCache(token: 'abc'); - }: isMethodCall('clearAuthCache', arguments: { - 'token': 'abc', - }), - () { - googleSignIn.requestScopes(['newScope', 'anotherScope']); - }: isMethodCall('requestScopes', arguments: { - 'scopes': ['newScope', 'anotherScope'], - }), - googleSignIn.signOut: isMethodCall('signOut', arguments: null), - googleSignIn.disconnect: isMethodCall('disconnect', arguments: null), - googleSignIn.isSignedIn: isMethodCall('isSignedIn', arguments: null), - }; + test('initWithParams passes arguments', () async { + const SignInInitParameters initParams = SignInInitParameters( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId', + ); - for (final void Function() f in tests.keys) { - f(); - } + await googleSignIn.init( + hostedDomain: initParams.hostedDomain, + scopes: initParams.scopes, + signInOption: initParams.signInOption, + clientId: initParams.clientId, + ); - expect(log, tests.values); + final VerificationResult result = verify(api.init(captureAny)); + final InitParams passedParams = result.captured[0] as InitParams; + expect(passedParams.hostedDomain, initParams.hostedDomain); + expect(passedParams.scopes, initParams.scopes); + expect(passedParams.signInType, SignInType.games); + expect(passedParams.clientId, initParams.clientId); + // These should use whatever the SignInInitParameters defaults are. + expect(passedParams.serverClientId, initParams.serverClientId); + expect(passedParams.forceCodeForRefreshToken, + initParams.forceCodeForRefreshToken); + }); + + test('initWithParams passes arguments', () async { + const SignInInitParameters initParams = SignInInitParameters( + hostedDomain: 'example.com', + scopes: ['two', 'scopes'], + signInOption: SignInOption.games, + clientId: 'fakeClientId', + serverClientId: 'fakeServerClientId', + forceCodeForRefreshToken: true, + ); + + await googleSignIn.initWithParams(initParams); + + final VerificationResult result = verify(api.init(captureAny)); + final InitParams passedParams = result.captured[0] as InitParams; + expect(passedParams.hostedDomain, initParams.hostedDomain); + expect(passedParams.scopes, initParams.scopes); + expect(passedParams.signInType, SignInType.games); + expect(passedParams.clientId, initParams.clientId); + expect(passedParams.serverClientId, initParams.serverClientId); + expect(passedParams.forceCodeForRefreshToken, + initParams.forceCodeForRefreshToken); + }); + + test('clearAuthCache passes arguments', () async { + const String token = 'abc'; + + await googleSignIn.clearAuthCache(token: token); + + verify(api.clearAuthCache(token)); + }); + + test('requestScopens passes arguments', () async { + const List scopes = ['newScope', 'anotherScope']; + when(api.requestScopes(scopes)).thenAnswer((_) async => true); + + final bool response = await googleSignIn.requestScopes(scopes); + + expect(response, true); + }); + + test('signOut calls through', () async { + await googleSignIn.signOut(); + + verify(api.signOut()); + }); + + test('disconnect calls through', () async { + await googleSignIn.disconnect(); + + verify(api.disconnect()); + }); + + test('isSignedIn passes true response', () async { + when(api.isSignedIn()).thenAnswer((_) async => true); + + expect(await googleSignIn.isSignedIn(), true); + }); + + test('isSignedIn passes false response', () async { + when(api.isSignedIn()).thenAnswer((_) async => false); + + expect(await googleSignIn.isSignedIn(), false); }); } - -/// This allows a value of type T or T? to be treated as a value of type T?. -/// -/// We use this so that APIs that have become non-nullable can still be used -/// with `!` and `?` on the stable branch. -T? _ambiguate(T? value) => value; diff --git a/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart new file mode 100644 index 0000000000..b6a5fed371 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_android/test/google_sign_in_android_test.mocks.dart @@ -0,0 +1,138 @@ +// Mocks generated by Mockito 5.4.1 from annotations +// in google_sign_in_android/test/google_sign_in_android_test.dart. +// Do not manually edit this file. + +// @dart=2.19 + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:google_sign_in_android/src/messages.g.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +class _FakeUserData_0 extends _i1.SmartFake implements _i2.UserData { + _FakeUserData_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +/// A class which mocks [GoogleSignInApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGoogleSignInApi extends _i1.Mock implements _i2.GoogleSignInApi { + MockGoogleSignInApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future init(_i2.InitParams? arg_params) => (super.noSuchMethod( + Invocation.method( + #init, + [arg_params], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future<_i2.UserData> signInSilently() => (super.noSuchMethod( + Invocation.method( + #signInSilently, + [], + ), + returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( + this, + Invocation.method( + #signInSilently, + [], + ), + )), + ) as _i3.Future<_i2.UserData>); + @override + _i3.Future<_i2.UserData> signIn() => (super.noSuchMethod( + Invocation.method( + #signIn, + [], + ), + returnValue: _i3.Future<_i2.UserData>.value(_FakeUserData_0( + this, + Invocation.method( + #signIn, + [], + ), + )), + ) as _i3.Future<_i2.UserData>); + @override + _i3.Future getAccessToken( + String? arg_email, + bool? arg_shouldRecoverAuth, + ) => + (super.noSuchMethod( + Invocation.method( + #getAccessToken, + [ + arg_email, + arg_shouldRecoverAuth, + ], + ), + returnValue: _i3.Future.value(''), + ) as _i3.Future); + @override + _i3.Future signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future disconnect() => (super.noSuchMethod( + Invocation.method( + #disconnect, + [], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future isSignedIn() => (super.noSuchMethod( + Invocation.method( + #isSignedIn, + [], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + @override + _i3.Future clearAuthCache(String? arg_token) => (super.noSuchMethod( + Invocation.method( + #clearAuthCache, + [arg_token], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + @override + _i3.Future requestScopes(List? arg_scopes) => + (super.noSuchMethod( + Invocation.method( + #requestScopes, + [arg_scopes], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); +}