Skip to content

Commit 7173bcf

Browse files
emersssoEgor
authored and
Egor
committed
[google_sign_in] Add implementations of requestScopes. (flutter#2599)
1 parent 1aa6634 commit 7173bcf

File tree

17 files changed

+535
-20
lines changed

17 files changed

+535
-20
lines changed

packages/google_sign_in/google_sign_in/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 4.3.0
2+
3+
* Add support for method introduced in `google_sign_in_platform_interface` 1.1.0.
4+
15
## 4.2.0
26

37
* Migrate to AndroidX.

packages/google_sign_in/google_sign_in/android/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66

77
import android.accounts.Account;
88
import android.app.Activity;
9+
import android.content.Context;
910
import android.content.Intent;
11+
import androidx.annotation.VisibleForTesting;
1012
import com.google.android.gms.auth.GoogleAuthUtil;
1113
import com.google.android.gms.auth.UserRecoverableAuthException;
1214
import com.google.android.gms.auth.api.signin.GoogleSignIn;
@@ -27,6 +29,7 @@
2729
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
2830
import io.flutter.plugin.common.MethodChannel.Result;
2931
import io.flutter.plugin.common.PluginRegistry;
32+
import java.util.ArrayList;
3033
import java.util.HashMap;
3134
import java.util.List;
3235
import java.util.Map;
@@ -46,17 +49,19 @@ public class GoogleSignInPlugin implements MethodCallHandler {
4649
private static final String METHOD_DISCONNECT = "disconnect";
4750
private static final String METHOD_IS_SIGNED_IN = "isSignedIn";
4851
private static final String METHOD_CLEAR_AUTH_CACHE = "clearAuthCache";
52+
private static final String METHOD_REQUEST_SCOPES = "requestScopes";
4953

5054
private final IDelegate delegate;
5155

5256
public static void registerWith(PluginRegistry.Registrar registrar) {
5357
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
54-
final GoogleSignInPlugin instance = new GoogleSignInPlugin(registrar);
58+
final GoogleSignInPlugin instance =
59+
new GoogleSignInPlugin(registrar, new GoogleSignInWrapper());
5560
channel.setMethodCallHandler(instance);
5661
}
5762

58-
private GoogleSignInPlugin(PluginRegistry.Registrar registrar) {
59-
delegate = new Delegate(registrar);
63+
GoogleSignInPlugin(PluginRegistry.Registrar registrar, GoogleSignInWrapper googleSignInWrapper) {
64+
delegate = new Delegate(registrar, googleSignInWrapper);
6065
}
6166

6267
@Override
@@ -100,6 +105,11 @@ public void onMethodCall(MethodCall call, Result result) {
100105
delegate.isSignedIn(result);
101106
break;
102107

108+
case METHOD_REQUEST_SCOPES:
109+
List<String> scopes = call.argument("scopes");
110+
delegate.requestScopes(result, scopes);
111+
break;
112+
103113
default:
104114
result.notImplemented();
105115
}
@@ -153,6 +163,9 @@ public void init(
153163

154164
/** Checks if there is a signed in user. */
155165
public void isSignedIn(Result result);
166+
167+
/** Prompts the user to grant an additional Oauth scopes. */
168+
public void requestScopes(final Result result, final List<String> scopes);
156169
}
157170

158171
/**
@@ -167,6 +180,7 @@ public void init(
167180
public static final class Delegate implements IDelegate, PluginRegistry.ActivityResultListener {
168181
private static final int REQUEST_CODE_SIGNIN = 53293;
169182
private static final int REQUEST_CODE_RECOVER_AUTH = 53294;
183+
@VisibleForTesting static final int REQUEST_CODE_REQUEST_SCOPE = 53295;
170184

171185
private static final String ERROR_REASON_EXCEPTION = "exception";
172186
private static final String ERROR_REASON_STATUS = "status";
@@ -183,13 +197,15 @@ public static final class Delegate implements IDelegate, PluginRegistry.Activity
183197

184198
private final PluginRegistry.Registrar registrar;
185199
private final BackgroundTaskRunner backgroundTaskRunner = new BackgroundTaskRunner(1);
200+
private final GoogleSignInWrapper googleSignInWrapper;
186201

187202
private GoogleSignInClient signInClient;
188203
private List<String> requestedScopes;
189204
private PendingOperation pendingOperation;
190205

191-
public Delegate(PluginRegistry.Registrar registrar) {
206+
public Delegate(PluginRegistry.Registrar registrar, GoogleSignInWrapper googleSignInWrapper) {
192207
this.registrar = registrar;
208+
this.googleSignInWrapper = googleSignInWrapper;
193209
registrar.addActivityResultListener(this);
194210
}
195211

@@ -343,6 +359,37 @@ public void isSignedIn(final Result result) {
343359
result.success(value);
344360
}
345361

362+
@Override
363+
public void requestScopes(Result result, List<String> scopes) {
364+
checkAndSetPendingOperation(METHOD_REQUEST_SCOPES, result);
365+
366+
GoogleSignInAccount account = googleSignInWrapper.getLastSignedInAccount(registrar.context());
367+
if (account == null) {
368+
result.error(ERROR_REASON_SIGN_IN_REQUIRED, "No account to grant scopes.", null);
369+
return;
370+
}
371+
372+
List<Scope> wrappedScopes = new ArrayList<>();
373+
374+
for (String scope : scopes) {
375+
Scope wrappedScope = new Scope(scope);
376+
if (!googleSignInWrapper.hasPermissions(account, wrappedScope)) {
377+
wrappedScopes.add(wrappedScope);
378+
}
379+
}
380+
381+
if (wrappedScopes.isEmpty()) {
382+
result.success(true);
383+
return;
384+
}
385+
386+
googleSignInWrapper.requestPermissions(
387+
registrar.activity(),
388+
REQUEST_CODE_REQUEST_SCOPE,
389+
account,
390+
wrappedScopes.toArray(new Scope[0]));
391+
}
392+
346393
private void onSignInResult(Task<GoogleSignInAccount> completedTask) {
347394
try {
348395
GoogleSignInAccount account = completedTask.getResult(ApiException.class);
@@ -527,9 +574,37 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
527574
finishWithError(ERROR_REASON_SIGN_IN_FAILED, "Signin failed");
528575
}
529576
return true;
577+
case REQUEST_CODE_REQUEST_SCOPE:
578+
finishWithSuccess(resultCode == Activity.RESULT_OK);
579+
return true;
530580
default:
531581
return false;
532582
}
533583
}
534584
}
535585
}
586+
587+
/**
588+
* A wrapper object that calls static method in GoogleSignIn.
589+
*
590+
* <p>Because GoogleSignIn uses static method mostly, which is hard for unit testing. We use this
591+
* wrapper class to use instance method which calls the corresponding GoogleSignIn static methods.
592+
*
593+
* <p>Warning! This class should stay true that each method calls a GoogleSignIn static method with
594+
* the same name and same parameters.
595+
*/
596+
class GoogleSignInWrapper {
597+
598+
GoogleSignInAccount getLastSignedInAccount(Context context) {
599+
return GoogleSignIn.getLastSignedInAccount(context);
600+
}
601+
602+
boolean hasPermissions(GoogleSignInAccount account, Scope scope) {
603+
return GoogleSignIn.hasPermissions(account, scope);
604+
}
605+
606+
void requestPermissions(
607+
Activity activity, int requestCode, GoogleSignInAccount account, Scope[] scopes) {
608+
GoogleSignIn.requestPermissions(activity, requestCode, account, scopes);
609+
}
610+
}

packages/google_sign_in/google_sign_in/example/android/app/build.gradle

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,18 @@ android {
4747
signingConfig signingConfigs.debug
4848
}
4949
}
50+
51+
testOptions {
52+
unitTests.returnDefaultValues = true
53+
}
5054
}
5155

5256
flutter {
5357
source '../..'
5458
}
59+
60+
dependencies {
61+
implementation 'com.google.android.gms:play-services-auth:16.0.1'
62+
testImplementation'junit:junit:4.12'
63+
testImplementation 'org.mockito:mockito-core:2.17.0'
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
package io.flutter.plugins.googlesignin;
6+
7+
import static org.mockito.Mockito.verify;
8+
import static org.mockito.Mockito.when;
9+
10+
import android.app.Activity;
11+
import android.content.Context;
12+
import android.content.Intent;
13+
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
14+
import com.google.android.gms.common.api.Scope;
15+
import io.flutter.plugin.common.BinaryMessenger;
16+
import io.flutter.plugin.common.MethodCall;
17+
import io.flutter.plugin.common.MethodChannel;
18+
import io.flutter.plugin.common.PluginRegistry;
19+
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;
20+
import io.flutter.plugins.googlesignin.GoogleSignInPlugin.Delegate;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.List;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
import org.mockito.ArgumentCaptor;
27+
import org.mockito.Mock;
28+
import org.mockito.MockitoAnnotations;
29+
import org.mockito.Spy;
30+
31+
public class GoogleSignInPluginTests {
32+
33+
@Mock Context mockContext;
34+
@Mock Activity mockActivity;
35+
@Mock PluginRegistry.Registrar mockRegistrar;
36+
@Mock BinaryMessenger mockMessenger;
37+
@Spy MethodChannel.Result result;
38+
@Mock GoogleSignInWrapper mockGoogleSignIn;
39+
@Mock GoogleSignInAccount account;
40+
private GoogleSignInPlugin plugin;
41+
42+
@Before
43+
public void setUp() {
44+
MockitoAnnotations.initMocks(this);
45+
when(mockRegistrar.messenger()).thenReturn(mockMessenger);
46+
when(mockRegistrar.context()).thenReturn(mockContext);
47+
when(mockRegistrar.activity()).thenReturn(mockActivity);
48+
plugin = new GoogleSignInPlugin(mockRegistrar, mockGoogleSignIn);
49+
}
50+
51+
@Test
52+
public void requestScopes_ResultErrorIfAccountIsNull() {
53+
MethodCall methodCall = new MethodCall("requestScopes", null);
54+
when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null);
55+
plugin.onMethodCall(methodCall, result);
56+
verify(result).error("sign_in_required", "No account to grant scopes.", null);
57+
}
58+
59+
@Test
60+
public void requestScopes_ResultTrueIfAlreadyGranted() {
61+
HashMap<String, List<String>> arguments = new HashMap<>();
62+
arguments.put("scopes", Collections.singletonList("requestedScope"));
63+
64+
MethodCall methodCall = new MethodCall("requestScopes", arguments);
65+
Scope requestedScope = new Scope("requestedScope");
66+
when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
67+
when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
68+
when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true);
69+
70+
plugin.onMethodCall(methodCall, result);
71+
verify(result).success(true);
72+
}
73+
74+
@Test
75+
public void requestScopes_RequestsPermissionIfNotGranted() {
76+
HashMap<String, List<String>> arguments = new HashMap<>();
77+
arguments.put("scopes", Collections.singletonList("requestedScope"));
78+
MethodCall methodCall = new MethodCall("requestScopes", arguments);
79+
Scope requestedScope = new Scope("requestedScope");
80+
81+
when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
82+
when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
83+
when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
84+
85+
plugin.onMethodCall(methodCall, result);
86+
87+
verify(mockGoogleSignIn)
88+
.requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope});
89+
}
90+
91+
@Test
92+
public void requestScopes_ReturnsFalseIfPermissionDenied() {
93+
HashMap<String, List<String>> arguments = new HashMap<>();
94+
arguments.put("scopes", Collections.singletonList("requestedScope"));
95+
MethodCall methodCall = new MethodCall("requestScopes", arguments);
96+
Scope requestedScope = new Scope("requestedScope");
97+
98+
ArgumentCaptor<ActivityResultListener> captor =
99+
ArgumentCaptor.forClass(ActivityResultListener.class);
100+
verify(mockRegistrar).addActivityResultListener(captor.capture());
101+
ActivityResultListener listener = captor.getValue();
102+
103+
when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
104+
when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
105+
when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
106+
107+
plugin.onMethodCall(methodCall, result);
108+
listener.onActivityResult(
109+
Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_CANCELED, new Intent());
110+
111+
verify(result).success(false);
112+
}
113+
114+
@Test
115+
public void requestScopes_ReturnsTrueIfPermissionGranted() {
116+
HashMap<String, List<String>> arguments = new HashMap<>();
117+
arguments.put("scopes", Collections.singletonList("requestedScope"));
118+
MethodCall methodCall = new MethodCall("requestScopes", arguments);
119+
Scope requestedScope = new Scope("requestedScope");
120+
121+
ArgumentCaptor<ActivityResultListener> captor =
122+
ArgumentCaptor.forClass(ActivityResultListener.class);
123+
verify(mockRegistrar).addActivityResultListener(captor.capture());
124+
ActivityResultListener listener = captor.getValue();
125+
126+
when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account);
127+
when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope));
128+
when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false);
129+
130+
plugin.onMethodCall(methodCall, result);
131+
listener.onActivityResult(
132+
Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent());
133+
134+
verify(result).success(true);
135+
}
136+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock-maker-inline
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleDevelopmentRegion</key>
6+
<string>$(DEVELOPMENT_LANGUAGE)</string>
7+
<key>CFBundleExecutable</key>
8+
<string>$(EXECUTABLE_NAME)</string>
9+
<key>CFBundleIdentifier</key>
10+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11+
<key>CFBundleInfoDictionaryVersion</key>
12+
<string>6.0</string>
13+
<key>CFBundleName</key>
14+
<string>$(PRODUCT_NAME)</string>
15+
<key>CFBundlePackageType</key>
16+
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
17+
<key>CFBundleShortVersionString</key>
18+
<string>1.0</string>
19+
<key>CFBundleVersion</key>
20+
<string>1</string>
21+
</dict>
22+
</plist>

0 commit comments

Comments
 (0)