How to correctly use Google Plus Sign In with multiple activities?

Solution 1:

Reconnecting for each activity is absolutely fine. Broadly there are 3 ways I've seen of people implementing this:

  1. Implement mostly in a baseactivity, and have the others extend that. This is connect/disconnect in each activity, but with code in only one place.
  2. Implement connect/disconnect in a fragment, and include that in activities where auth is needed. This is helpful if you already have a baseactivity you can't extend (e.g. some games cases).
  3. Implement a service to connect/disconnect. This can fire a broadcastintent or similar if sign in is required.

All of these work, and I've seen them all used in real world apps. The main thing to remember is to separate the 99% logic (user is either signed in or signed out, and you are being informed of that) from the relatively rare "signing in at this present moment" use-case. So for example, you might have onConnected/onConnection failed firing a lot, but mostly you are ignoring or just flipping a bit as to the state of the application. Only on a screen with a login button do you need the connection result resolution and onActivityResult stuff. Think of the google play services connection as being mostly about asking for the state of the user, rather than signing them in, and you should be fine.

Solution 2:

I agree with Ian Barber's answer but to explain a little further, your Activitys should be considered in two types - Activitys that resolve sign in, and Activitys that require sign in.

Most Activitys do not concern themselves with authenticating the user and will have the same logic in your app. They will create a GoogleApiClient, which connects to the Google Play services process running on the device and reads the cached sign-in state of the user - returning onConnected() if the user is signed in, and onConnectionFailed() if not. Most of your Activitys will want to reset your application state and start your LoginActivity if the user was not signed in. Each Activity should maintain its own instance of GoogleApiClient since it is a lightweight object used to access the shared state held by the Google Play services process. This behaviour could, for example, be encapsulated in a shared BaseActivity class or a shared SignInFragment class, but each instance should have its own GoogleApiClient instance.

Your LoginActivity needs to be implemented differently however. It should also create a GoogleApiClient, but when it receives onConnected() indicating the user is signed in, it should start an appropriate Activity for the user and finish(). When your LoginActivity receives onConnectionFailed() indicating the user is not signed in, you should attempt to resolve sign in issues with startResolutionForResult().

Solution 3:

0. TL;DR

For the impatient coder, a working version of the following implementation can be found on GitHub.

After rewriting the login activity code several times in many different apps, the easy (and not so elegant) solution was create the Google API client as a Application class object. But, since the connection state affect the UX flow, I never was happy about with this approach.

Reducing our problem only to the connection concept, we may consider that:

  1. It hides the Google API client.
  2. It has finite states.
  3. It is a (rather) unique.
  4. The current state affect the behavior of the app.

1. Proxy Pattern

Since the Connection encapsulates the GoogleApiClient, it will implement the ConnectionCallbacks and OnConnectionFailedListener:

@Override
public void onConnected(Bundle hint) {
    changeState(State.OPENED);
}

@Override
public void onConnectionSuspended(int cause) {
    changeState(State.CLOSED);
    connect();
}

@Override
public void onConnectionFailed(ConnectionResult result) {
    if (currentState.equals(State.CLOSED) && result.hasResolution()) {
        changeState(State.CREATED);
        connectionResult = result;
    } else {
        connect();
    }
}

Activities can communicate to the Connection class through the methods connect, disconnect, and revoke, but their behaviors are decided by the current state. The following methods are required by the state machine:

protected void onSignIn() {
    if (!googleApiClient.isConnected() && !googleApiClient.isConnecting()) {
        googleApiClient.connect();
    }
}

protected void onSignOut() {
    if (googleApiClient.isConnected()) {
        Plus.AccountApi.clearDefaultAccount(googleApiClient);
        googleApiClient.disconnect();
        googleApiClient.connect();
        changeState(State.CLOSED);
    }
}

protected void onSignUp() {
    Activity activity = activityWeakReference.get();
    try {
        changeState(State.OPENING);
        connectionResult.startResolutionForResult(activity, REQUEST_CODE);
    } catch (IntentSender.SendIntentException e) {
        changeState(State.CREATED);
        googleApiClient.connect();
    }
}

protected void onRevoke() {
    Plus.AccountApi.clearDefaultAccount(googleApiClient);
    Plus.AccountApi.revokeAccessAndDisconnect(googleApiClient);
    googleApiClient = googleApiClientBuilder.build();
    googleApiClient.connect();
    changeState(State.CLOSED);
}

2. State Pattern

This is a behavioral pattern the allow an object to alter its behavior when its internal state changes. The GoF Design Patterns book describes how a TCP connection can be represent by this pattern (which is also our case).

A state from a state machine should be a singleton, and the easiest away of doing it in Java was to create Enum named State as follows:

public enum State {
    CREATED {
        @Override
        void connect(Connection connection) {
            connection.onSignUp();
        }
        @Override
        void disconnect(Connection connection) {
            connection.onSignOut();
        }
    },
    OPENING {},
    OPENED {
        @Override
        void disconnect(Connection connection) {
            connection.onSignOut();
        }
        @Override
        void revoke(Connection connection) {
            connection.onRevoke();
        }
    },
    CLOSED {
        @Override
        void connect(Connection connection) {
            connection.onSignIn();
        }
    };

void connect(Connection connection) {}
void disconnect(Connection connection) {}
void revoke(Connection connection) {}

The Connection class holds the context, i.e. the current state, which defines how the Connection methods connect, disconnect, and revoke will behave:

public void connect() {
    currentState.connect(this);
}

public void disconnect() {
    currentState.disconnect(this);
}

public void revoke() {
    currentState.revoke(this);
}

private void changeState(State state) {
    currentState = state;
    setChanged();
    notifyObservers(state);
}

3. Singleton Pattern

Since there is not need to recreate this class repeatedly, we provide it as a singleton:

public static Connection getInstance(Activity activity) {
    if (null == sConnection) {
        sConnection = new Connection(activity);
    }

    return sConnection;
}

public void onActivityResult(int result) {
    if (result == Activity.RESULT_OK) {
        changeState(State.CREATED);
    } else {
        changeState(State.CLOSED);
    }
    onSignIn();
}

private Connection(Activity activity) {
    activityWeakReference = new WeakReference<>(activity);

    googleApiClientBuilder = new GoogleApiClient
           .Builder(activity)
           .addConnectionCallbacks(this)
           .addOnConnectionFailedListener(this)
           .addApi(Plus.API, Plus.PlusOptions.builder().build())
           .addScope(new Scope("email"));

    googleApiClient = googleApiClientBuilder.build();
    currentState = State.CLOSED;
}

4. Observable Pattern

The Connection class extends Java Observable, so 1 or more activities can observe the state changes:

@Override
protected void onCreate(Bundle bundle) {
    connection = Connection.getInstance(this);
    connection.addObserver(this);
}

@Override
protected void onStart() {
    connection.connect();
}

@Override
protected void onDestroy() {
    connection.deleteObserver(this);
    connection.disconnect();
}

@Override
protected void onActivityResult(int request, int result, Intent data) {
    if (Connection.REQUEST_CODE == request) {
        connection.onActivityResult(result);
    }
}

@Override
public void update(Observable observable, Object data) {
    if (observable != connection) {
        return;
    }
    // Your presentation logic goes here...
}