Skip to content

Commit

Permalink
Manually manage GoogleApiClient (flutter#148)
Browse files Browse the repository at this point in the history
* removed auto-manage

* ++

* clean-up

* small fixes

* Add changelog

* review comments
  • Loading branch information
goderbauer committed Jun 22, 2017
1 parent 4ff23eb commit 7b0c0a3
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 30 deletions.
6 changes: 6 additions & 0 deletions packages/google_sign_in/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 0.2.1

* Plugin can (once again) be used in apps that extend `FlutterActivity`
* `signInSilently` is guaranteed to never throw
* A failed sign-in (caused by a failing `init` step) will no longer block subsequent sign-in attempts

## 0.2.0

* Updated dependencies
Expand Down
1 change: 0 additions & 1 deletion packages/google_sign_in/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ android {
}

dependencies {
compile 'com.android.support:support-v4:25.4.0'
compile 'com.google.android.gms:play-services-auth:11.0.1'
compile 'com.google.guava:guava:20.0'
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@

import android.accounts.Account;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.api.Auth;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
import com.google.android.gms.auth.api.signin.GoogleSignInResult;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.OptionalPendingResult;
Expand All @@ -27,7 +30,6 @@
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import io.flutter.app.FlutterFragmentActivity;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
Expand All @@ -50,6 +52,7 @@ public class GoogleSignInPlugin
GoogleApiClient.OnConnectionFailedListener {

private static final int REQUEST_CODE = 53293;
private static final int REQUEST_CODE_RESOLVE_ERROR = 1001;

private static final String CHANNEL_NAME = "plugins.flutter.io/google_sign_in";

Expand All @@ -61,6 +64,8 @@ public class GoogleSignInPlugin
private static final String ERROR_REASON_OPERATION_IN_PROGRESS = "operation_in_progress";
private static final String ERROR_REASON_CONNECTION_FAILED = "connection_failed";

private static final String STATE_RESOLVING_ERROR = "resolving_error";

private static final String METHOD_INIT = "init";
private static final String METHOD_SIGN_IN_SILENTLY = "signInSilently";
private static final String METHOD_SIGN_IN = "signIn";
Expand All @@ -79,37 +84,32 @@ private static final class PendingOperation {
}
}

private final FragmentActivity activity;
private final Activity activity;
private final BackgroundTaskRunner backgroundTaskRunner;
private final int requestCode;

private boolean resolvingError = false; // Whether we are currently resolving a sign-in error
private GoogleApiClient googleApiClient;
private List<String> requestedScopes;
private PendingOperation pendingOperation;

public static void registerWith(PluginRegistry.Registrar registrar) {
Activity activity = registrar.activity();
if (!(activity instanceof FragmentActivity)) {
throw new IllegalArgumentException(
GoogleSignInPlugin.class.getSimpleName()
+ " requires your activity to be an instance of "
+ FragmentActivity.class.getName()
+ ". You may want to use "
+ FlutterFragmentActivity.class.getName());
}
FragmentActivity fragmentActivity = (FragmentActivity) activity;
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
final GoogleSignInPlugin instance =
new GoogleSignInPlugin(fragmentActivity, new BackgroundTaskRunner(1), REQUEST_CODE);
new GoogleSignInPlugin(activity, new BackgroundTaskRunner(1), REQUEST_CODE);
registrar.addActivityResultListener(instance);
channel.setMethodCallHandler(instance);
}

private GoogleSignInPlugin(
FragmentActivity activity, BackgroundTaskRunner backgroundTaskRunner, int requestCode) {
Activity activity, BackgroundTaskRunner backgroundTaskRunner, int requestCode) {
this.activity = activity;
this.backgroundTaskRunner = backgroundTaskRunner;
this.requestCode = requestCode;
activity
.getApplication()
.registerActivityLifecycleCallbacks(new GoogleApiClientConnectionManager());
}

@Override
Expand Down Expand Up @@ -155,7 +155,6 @@ private void init(Result result, List<String> requestedScopes, String hostedDoma
try {
if (googleApiClient != null) {
// This can happen if the scopes change, or a full restart hot reload
googleApiClient.stopAutoManage(activity);
googleApiClient = null;
}
GoogleSignInOptions.Builder optionsBuilder =
Expand All @@ -181,11 +180,11 @@ private void init(Result result, List<String> requestedScopes, String hostedDoma
this.requestedScopes = requestedScopes;
this.googleApiClient =
new GoogleApiClient.Builder(activity)
.enableAutoManage(activity, this)
.addApi(Auth.GOOGLE_SIGN_IN_API, optionsBuilder.build())
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
this.googleApiClient.connect();
} catch (Exception e) {
Log.e(TAG, "Initialization error", e);
result.error(ERROR_REASON_EXCEPTION, e.getMessage(), null);
Expand Down Expand Up @@ -372,10 +371,34 @@ public void onConnected(Bundle connectionHint) {
* properly initialize this listener.
*/
@Override
public void onConnectionFailed(@NonNull ConnectionResult result) {
// We can attempt to reconnect if, e.g. the activity is paused and resumed.
if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
finishWithError(ERROR_REASON_CONNECTION_FAILED, result.toString());
public void onConnectionFailed(@NonNull final ConnectionResult result) {
if (resolvingError) {
// Already attempting to resolve an error.
return;
} else if (result.hasResolution()) {
try {
resolvingError = true;
result.startResolutionForResult(activity, REQUEST_CODE_RESOLVE_ERROR);
} catch (SendIntentException e) {
resolvingError = false;
finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
}
} else {
resolvingError = true;
GoogleApiAvailability.getInstance()
.showErrorDialogFragment(
activity,
result.getErrorCode(),
REQUEST_CODE_RESOLVE_ERROR,
new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
finishWithError(
ERROR_REASON_CONNECTION_FAILED, String.valueOf(result.getErrorCode()));
}
resolvingError = false;
}
});
}
}

Expand All @@ -387,6 +410,20 @@ public void onConnectionSuspended(int cause) {

@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_RESOLVE_ERROR) {
// Deal with result of `onConnectionFailed` error resolution.
resolvingError = false;
if (resultCode == Activity.RESULT_OK) {
// Make sure the app is not already connected or attempting to connect
if (!googleApiClient.isConnecting() && !googleApiClient.isConnected()) {
googleApiClient.connect();
}
} else if (pendingOperation != null && pendingOperation.method.equals(METHOD_INIT)) {
finishWithError(ERROR_REASON_CONNECTION_FAILED, String.valueOf(resultCode));
}
return true;
}

if (requestCode != this.requestCode) {
// We're only interested in the "sign in" activity result
return false;
Expand Down Expand Up @@ -439,4 +476,41 @@ private void finishWithError(String errorCode, String errorMessage) {
}
pendingOperation = null;
}

private class GoogleApiClientConnectionManager implements ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle bundle) {
resolvingError = bundle != null && bundle.getBoolean(STATE_RESOLVING_ERROR, false);
}

@Override
public void onActivityDestroyed(Activity activity) {}

@Override
public void onActivityPaused(Activity activity) {}

@Override
public void onActivityResumed(Activity activity) {}

@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
outState.putBoolean(STATE_RESOLVING_ERROR, resolvingError);
}

@Override
public void onActivityStarted(Activity activity) {
if (!resolvingError
&& activity == GoogleSignInPlugin.this.activity
&& googleApiClient != null) {
googleApiClient.connect();
}
}

@Override
public void onActivityStopped(Activity activity) {
if (activity == GoogleSignInPlugin.this.activity && googleApiClient != null) {
googleApiClient.disconnect();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
package io.flutter.plugins.googlesigninexample;

import android.os.Bundle;
import io.flutter.app.FlutterFragmentActivity;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterFragmentActivity {
public class MainActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down
14 changes: 11 additions & 3 deletions packages/google_sign_in/lib/google_sign_in.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ class GoogleSignIn {
_initialization = channel.invokeMethod("init", <String, dynamic>{
'scopes': scopes ?? <String>[],
'hostedDomain': hostedDomain,
});
})
..catchError((dynamic _) {
// Invalidate initialization if it errored out.
_initialization = null;
});
}
await _initialization;
final Map<String, dynamic> response = await channel.invokeMethod(method);
Expand Down Expand Up @@ -201,8 +205,12 @@ class GoogleSignIn {
/// a Future which resolves to the same user instance.
///
/// Re-authentication can be triggered only after [signOut] or [disconnect].
Future<GoogleSignInAccount> signInSilently() =>
_addMethodCall('signInSilently');
Future<GoogleSignInAccount> signInSilently() {
return _addMethodCall('signInSilently').catchError((dynamic _) {
// ignore, we promised to be silent.
// TODO(goderbauer): revisit when the native side throws less aggressively.
});
}

/// Starts the interactive sign-in process.
///
Expand Down
2 changes: 1 addition & 1 deletion packages/google_sign_in/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: google_sign_in
description: Google Sign-In plugin for Flutter.
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in
version: 0.2.0
version: 0.2.1

flutter:
plugin:
Expand Down
28 changes: 25 additions & 3 deletions packages/google_sign_in/test/google_sign_in_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:google_sign_in/google_sign_in.dart';
import 'package:test/test.dart';

void main() {
group('$GoogleSignIn', () {
group('GoogleSignIn', () {
const MethodChannel channel = const MethodChannel(
'plugins.flutter.io/google_sign_in',
);
Expand Down Expand Up @@ -140,8 +140,7 @@ void main() {

test('can sign in after previously failed attempt', () async {
responses['signInSilently'] = <String, dynamic>{'error': 'Not a user'};
expect(googleSignIn.signInSilently(),
throwsA(const isInstanceOf<AssertionError>()));
expect(await googleSignIn.signInSilently(), isNull);
expect(await googleSignIn.signIn(), isNotNull);
expect(
log,
Expand Down Expand Up @@ -228,5 +227,28 @@ void main() {
const MethodCall('disconnect'),
]));
});

test('signInSilently does not throw on error', () async {
channel.setMockMethodCallHandler((MethodCall methodCall) {
throw "I am an error";
});
expect(await googleSignIn.signInSilently(), isNull); // should not throw
});

test('can sign in after init failed before', () async {
int initCount = 0;
channel.setMockMethodCallHandler((MethodCall methodCall) {
if (methodCall.method == 'init') {
initCount++;
if (initCount == 1) {
throw "First init fails";
}
}
return responses[methodCall.method];
});
expect(googleSignIn.signIn(),
throwsA(const isInstanceOf<PlatformException>()));
expect(await googleSignIn.signIn(), isNotNull);
});
});
}

0 comments on commit 7b0c0a3

Please sign in to comment.