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<String> 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<String> 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<String> 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<String> scopes); + void requestScopes( + final @NonNull MethodChannel.Result result, final @NonNull List<String> 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 <T> The Result type of the result to convert from. + */ + private abstract static class ErrorConvertingMethodChannelResult<T> + implements Messages.Result<T> { + 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<Messages.UserData> { + public UserDataMethodChannelResult(MethodChannel.Result result) { + super(result); + } + + @Override + public void success(Messages.UserData data) { + Map<String, Object> 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<Void> { + 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. * * <p>All methods in this class assume that they are run to completion before any other method is - * invoked. In this context, "run to completion" means that their {@link Result} argument has been - * completed (either successfully or in error). This class provides no synchronization 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<Messages.UserData> userDataResult, + Messages.Result<Void> voidResult, + Messages.Result<Boolean> boolResult, + Messages.Result<String> 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<Messages.UserData> result) { + checkAndSetPendingOperation(method, result, null, null, null, null); + } + + private void checkAndSetPendingVoidOperation( + String method, @NonNull Messages.Result<Void> result) { + checkAndSetPendingOperation(method, null, result, null, null, null); + } + + private void checkAndSetPendingBoolOperation( + String method, @NonNull Messages.Result<Boolean> result) { + checkAndSetPendingOperation(method, null, null, result, null, null); + } + + private void checkAndSetPendingStringOperation( + String method, @NonNull Messages.Result<String> result, @Nullable Object data) { + checkAndSetPendingOperation(method, null, null, null, result, data); + } + + private void checkAndSetPendingAccessTokenOperation( + String method, Messages.Result<String> 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<String> 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<String> 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<Messages.UserData> result) { + checkAndSetPendingSignInOperation("signInSilently", result); Task<GoogleSignInAccount> 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<Messages.UserData> 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<Void> 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<Void> 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<String> scopes) { - checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result); + public void requestScopes( + @NonNull List<String> scopes, @NonNull Messages.Result<Boolean> 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<String> scopes) { + requestScopes( + scopes, + new ErrorConvertingMethodChannelResult<Boolean>(result) { + @Override + public void success(Boolean value) { + result.success(value); + } + }); + } + private void onSignInResult(Task<GoogleSignInAccount> 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<String, Object> 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<Messages.UserData> userDataResult; + final @Nullable Messages.Result<Void> voidResult; + final @Nullable Messages.Result<Boolean> boolResult; + final @Nullable Messages.Result<String> stringResult; + final @Nullable Object data; - PendingOperation(String method, Result result, Object data) { + PendingOperation( + @NonNull String method, + @Nullable Messages.Result<Messages.UserData> userDataResult, + @Nullable Messages.Result<Void> voidResult, + @Nullable Messages.Result<Boolean> boolResult, + @Nullable Messages.Result<String> 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<Void> result) { Callable<Void> 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<String> result) { Callable<String> 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<String, String> 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<String>(result) { + @Override + public void success(String value) { + HashMap<String, String> 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<String> 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<Object> wrapError(@NonNull Throwable exception) { + ArrayList<Object> errorList = new ArrayList<Object>(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. + * + * <p>See SignInInitParams for details. + * + * <p>Generated class from Pigeon that represents data sent in messages. + */ + public static final class InitParams { + private @NonNull List<String> scopes; + + public @NonNull List<String> getScopes() { + return scopes; + } + + public void setScopes(@NonNull List<String> 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<String> scopes; + + public @NonNull Builder setScopes(@NonNull List<String> 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<Object> toList() { + ArrayList<Object> toListResult = new ArrayList<Object>(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<Object> list) { + InitParams pigeonResult = new InitParams(); + Object scopes = list.get(0); + pigeonResult.setScopes((List<String>) 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. + * + * <p>See GoogleSignInUserData for details. + * + * <p>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<Object> toList() { + ArrayList<Object> toListResult = new ArrayList<Object>(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<Object> 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<T> { + @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<Object>) readValue(buffer)); + case (byte) 129: + return UserData.fromList((ArrayList<Object>) 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<UserData> result); + /** Starts a sign in with user interaction. */ + void signIn(@NonNull Result<UserData> result); + /** Requests the access token for the current sign in. */ + void getAccessToken( + @NonNull String email, @NonNull Boolean shouldRecoverAuth, @NonNull Result<String> result); + /** Signs out the current user. */ + void signOut(@NonNull Result<Void> result); + /** Revokes scope grants to the application. */ + void disconnect(@NonNull Result<Void> 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<Void> result); + /** Requests access to the given scopes. */ + void requestScopes(@NonNull List<String> scopes, @NonNull Result<Boolean> result); + + /** The codec used by GoogleSignInApi. */ + static @NonNull MessageCodec<Object> 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<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.init", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + ArrayList<Object> args = (ArrayList<Object>) message; + InitParams paramsArg = (InitParams) args.get(0); + try { + api.init(paramsArg); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList<Object> wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signInSilently", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + Result<UserData> resultCallback = + new Result<UserData>() { + public void success(UserData result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signInSilently(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signIn", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + Result<UserData> resultCallback = + new Result<UserData>() { + public void success(UserData result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signIn(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.getAccessToken", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + ArrayList<Object> args = (ArrayList<Object>) message; + String emailArg = (String) args.get(0); + Boolean shouldRecoverAuthArg = (Boolean) args.get(1); + Result<String> resultCallback = + new Result<String>() { + public void success(String result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.getAccessToken(emailArg, shouldRecoverAuthArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.signOut", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + Result<Void> resultCallback = + new Result<Void>() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.signOut(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.disconnect", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + Result<Void> resultCallback = + new Result<Void>() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.disconnect(resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.isSignedIn", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + try { + Boolean output = api.isSignedIn(); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList<Object> wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.clearAuthCache", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + ArrayList<Object> args = (ArrayList<Object>) message; + String tokenArg = (String) args.get(0); + Result<Void> resultCallback = + new Result<Void>() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.clearAuthCache(tokenArg, resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel<Object> channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.GoogleSignInApi.requestScopes", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList<Object> wrapped = new ArrayList<Object>(); + ArrayList<Object> args = (ArrayList<Object>) message; + List<String> scopesArg = (List<String>) args.get(0); + Result<Boolean> resultCallback = + new Result<Boolean>() { + public void success(Boolean result) { + wrapped.add(0, result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList<Object> 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<GoogleSignInAccount> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<GoogleSignInOptions> 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<GoogleSignInOptions> 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.<String>emptyList(), clientId, serverClientId, false); + } + + private static MethodCall buildInitMethodCall( + String clientId, String serverClientId, boolean forceCodeForRefreshToken) { + return buildInitMethodCall( + "SignInOption.standard", + Collections.<String>emptyList(), + clientId, + serverClientId, + forceCodeForRefreshToken); + } + + private static MethodCall buildInitMethodCall( + String signInOption, + List<String> scopes, + String clientId, + String serverClientId, + boolean forceCodeForRefreshToken) { + HashMap<String, Object> 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<Void> voidResult; + @Spy Messages.Result<Boolean> boolResult; + @Spy Messages.Result<Messages.UserData> 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<String, List<String>> 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<Throwable> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); + List<String> requestedScopes = Collections.singletonList("requestedScope"); Scope requestedScope = new Scope("requestedScope"); - ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<String, List<String>> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - + List<String> requestedScopes = Collections.singletonList("requestedScope"); ArgumentCaptor<PluginRegistry.ActivityResultListener> 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<Throwable> resultCaptor = ArgumentCaptor.forClass(Throwable.class); + verify(boolResult, times(2)).error(resultCaptor.capture()); + List<Throwable> 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<Throwable> 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<GoogleSignInOptions> 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<GoogleSignInOptions> 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.<String>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.<String>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<String> scopes, String clientId, String serverClientId, boolean forceCodeForRefreshToken) { - HashMap<String, Object> 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<void> initWithParams(SignInInitParameters params) { - return channel.invokeMethod<void>('init', <String, dynamic>{ - '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<GoogleSignInUserData?> signInSilently() { - return channel - .invokeMapMethod<String, dynamic>('signInSilently') - .then(getUserDataFromMap); + return _api.signInSilently().then(_signInUserDataFromChannelData); } @override Future<GoogleSignInUserData?> signIn() { - return channel - .invokeMapMethod<String, dynamic>('signIn') - .then(getUserDataFromMap); + return _api.signIn().then(_signInUserDataFromChannelData); } @override Future<GoogleSignInTokenData> getTokens( {required String email, bool? shouldRecoverAuth = true}) { - return channel - .invokeMapMethod<String, dynamic>('getTokens', <String, dynamic>{ - 'email': email, - 'shouldRecoverAuth': shouldRecoverAuth ?? true, - }).then((Map<String, dynamic>? result) => getTokenDataFromMap(result!)); + return _api + .getAccessToken(email, shouldRecoverAuth ?? true) + .then((String result) => GoogleSignInTokenData( + accessToken: result, + )); } @override Future<void> signOut() { - return channel.invokeMapMethod<String, dynamic>('signOut'); + return _api.signOut(); } @override Future<void> disconnect() { - return channel.invokeMapMethod<String, dynamic>('disconnect'); + return _api.disconnect(); } @override - Future<bool> isSignedIn() async { - return (await channel.invokeMethod<bool>('isSignedIn'))!; + Future<bool> isSignedIn() { + return _api.isSignedIn(); } @override Future<void> clearAuthCache({String? token}) { - return channel.invokeMethod<void>( - 'clearAuthCache', - <String, String?>{'token': token}, - ); + // The token is not acutally nullable; see + // https://github.com/flutter/flutter/issues/129717 + return _api.clearAuthCache(token!); } @override - Future<bool> requestScopes(List<String> scopes) async { - return (await channel.invokeMethod<bool>( - 'requestScopes', - <String, List<String>>{'scopes': scopes}, - ))!; + Future<bool> requestScopes(List<String> 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<String?> scopes; + + SignInType signInType; + + String? hostedDomain; + + String? clientId; + + String? serverClientId; + + bool forceCodeForRefreshToken; + + Object encode() { + return <Object?>[ + scopes, + signInType.index, + hostedDomain, + clientId, + serverClientId, + forceCodeForRefreshToken, + ]; + } + + static InitParams decode(Object result) { + result as List<Object?>; + return InitParams( + scopes: (result[0] as List<Object?>?)!.cast<String?>(), + 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 <Object?>[ + displayName, + email, + id, + photoUrl, + idToken, + serverAuthCode, + ]; + } + + static UserData decode(Object result) { + result as List<Object?>; + 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<Object?> codec = _GoogleSignInApiCodec(); + + /// Initializes a sign in request with the given parameters. + Future<void> init(InitParams arg_params) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.init', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = + await channel.send(<Object?>[arg_params]) as List<Object?>?; + 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<UserData> signInSilently() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.signInSilently', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel.send(null) as List<Object?>?; + 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<UserData> signIn() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.signIn', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel.send(null) as List<Object?>?; + 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<String> getAccessToken( + String arg_email, bool arg_shouldRecoverAuth) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.getAccessToken', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel + .send(<Object?>[arg_email, arg_shouldRecoverAuth]) as List<Object?>?; + 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<void> signOut() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.signOut', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel.send(null) as List<Object?>?; + 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<void> disconnect() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.disconnect', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel.send(null) as List<Object?>?; + 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<bool> isSignedIn() async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.isSignedIn', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = await channel.send(null) as List<Object?>?; + 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<void> clearAuthCache(String arg_token) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.clearAuthCache', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = + await channel.send(<Object?>[arg_token]) as List<Object?>?; + 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<bool> requestScopes(List<String?> arg_scopes) async { + final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>( + 'dev.flutter.pigeon.GoogleSignInApi.requestScopes', codec, + binaryMessenger: _binaryMessenger); + final List<Object?>? replyList = + await channel.send(<Object?>[arg_scopes]) as List<Object?>?; + 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<String, dynamic>? 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<String, dynamic> 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 <String>[], + 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<String?> 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<String> 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<String, String> kUserData = <String, String>{ - '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<dynamic, dynamic> kTokenData = <String, dynamic>{ - 'idToken': '123', - 'accessToken': '456', - 'serverAuthCode': '789', -}; - -const Map<String, dynamic> kDefaultResponses = <String, dynamic>{ - '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<String, dynamic>); +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(<Type>[GoogleSignInApi]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final GoogleSignInAndroid googleSignIn = GoogleSignInAndroid(); - final MethodChannel channel = googleSignIn.channel; - - final List<MethodCall> log = <MethodCall>[]; - late Map<String, dynamic> - responses; // Some tests mutate some kDefaultResponses + late GoogleSignInAndroid googleSignIn; + late MockGoogleSignInApi api; setUp(() { - responses = Map<String, dynamic>.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<dynamic>.error('$response'); - } - return Future<dynamic>.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<PlatformException>())); }); 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<PlatformException>())); }); 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: <String, dynamic>{ - '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: <String, dynamic>{ - '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<void Function(), Matcher> tests = <void Function(), Matcher>{ - () { - googleSignIn.init( - hostedDomain: 'example.com', - scopes: <String>['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId'); - }: isMethodCall('init', arguments: <String, dynamic>{ - 'hostedDomain': 'example.com', - 'scopes': <String>['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': null, - 'forceCodeForRefreshToken': false, - }), - () { - googleSignIn.initWithParams(const SignInInitParameters( - hostedDomain: 'example.com', - scopes: <String>['two', 'scopes'], - signInOption: SignInOption.games, - clientId: 'fakeClientId', - serverClientId: 'fakeServerClientId', - forceCodeForRefreshToken: true)); - }: isMethodCall('init', arguments: <String, dynamic>{ - 'hostedDomain': 'example.com', - 'scopes': <String>['two', 'scopes'], - 'signInOption': 'SignInOption.games', - 'clientId': 'fakeClientId', - 'serverClientId': 'fakeServerClientId', - 'forceCodeForRefreshToken': true, - }), - () { - googleSignIn.getTokens( - email: 'example@example.com', shouldRecoverAuth: false); - }: isMethodCall('getTokens', arguments: <String, dynamic>{ - 'email': 'example@example.com', - 'shouldRecoverAuth': false, - }), - () { - googleSignIn.clearAuthCache(token: 'abc'); - }: isMethodCall('clearAuthCache', arguments: <String, dynamic>{ - 'token': 'abc', - }), - () { - googleSignIn.requestScopes(<String>['newScope', 'anotherScope']); - }: isMethodCall('requestScopes', arguments: <String, dynamic>{ - 'scopes': <String>['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: <String>['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: <String>['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<String> scopes = <String>['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>(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<void> init(_i2.InitParams? arg_params) => (super.noSuchMethod( + Invocation.method( + #init, + [arg_params], + ), + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); + @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<String> getAccessToken( + String? arg_email, + bool? arg_shouldRecoverAuth, + ) => + (super.noSuchMethod( + Invocation.method( + #getAccessToken, + [ + arg_email, + arg_shouldRecoverAuth, + ], + ), + returnValue: _i3.Future<String>.value(''), + ) as _i3.Future<String>); + @override + _i3.Future<void> signOut() => (super.noSuchMethod( + Invocation.method( + #signOut, + [], + ), + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); + @override + _i3.Future<void> disconnect() => (super.noSuchMethod( + Invocation.method( + #disconnect, + [], + ), + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); + @override + _i3.Future<bool> isSignedIn() => (super.noSuchMethod( + Invocation.method( + #isSignedIn, + [], + ), + returnValue: _i3.Future<bool>.value(false), + ) as _i3.Future<bool>); + @override + _i3.Future<void> clearAuthCache(String? arg_token) => (super.noSuchMethod( + Invocation.method( + #clearAuthCache, + [arg_token], + ), + returnValue: _i3.Future<void>.value(), + returnValueForMissingStub: _i3.Future<void>.value(), + ) as _i3.Future<void>); + @override + _i3.Future<bool> requestScopes(List<String?>? arg_scopes) => + (super.noSuchMethod( + Invocation.method( + #requestScopes, + [arg_scopes], + ), + returnValue: _i3.Future<bool>.value(false), + ) as _i3.Future<bool>); +}