[google_sign_in] Convert Android to Pigeon (#4344)

Replaces the direct method channel implementation with Pigeon.

Since `google_sign_in`, unlike most of our plugins, exposes an API that's intended for direct cross-plugin native use, the existing methods are all left in place, but refactored as passthroughs to the new Pigeon versions. To ensure that they aren't broken, the existing Java tests are preserved unchanged (as a "legacy" copy) with `onMethodCall` left in place for now just to allow the tests to continue to run as-is. Since that dispatches to the legacy methods, this keeps the existing coverage of those methods.

The new tests are a copy of the legacy tests, minimally translated to use the new Pigeon variants, to ensure continuity of testing to the new version.

Part of https://github.com/flutter/flutter/issues/117908
This commit is contained in:
stuartmorgan
2023-06-30 20:28:49 -04:00
committed by GitHub
parent d3c0d5f5e2
commit 6ab9a8bfb4
13 changed files with 2394 additions and 428 deletions

View File

@ -1,3 +1,7 @@
## 6.1.17
* Converts method channels to Pigeon.
## 6.1.16
* Updates Guava to version 32.0.1.

View File

@ -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;

View File

@ -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);
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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,
);
}
}

View File

@ -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?)!;
}
}
}

View File

@ -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?,
);
}

View File

@ -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.

View File

@ -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);
}

View File

@ -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:

View File

@ -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;

View File

@ -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>);
}