chromium/chrome/android/java/src/org/chromium/chrome/browser/gcore/GoogleApiClientHelper.java

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.gcore;

import android.os.Bundle;
import android.os.Handler;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks;
import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener;

import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ApplicationStateListener;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;

/**
 * Helps managing connections when using {@link GoogleApiClient}.
 *
 * It features:
 *
 * <ul>
 * <li>Connection failure handling: some connection failures can be solved by retrying. In those
 * cases, a few reconnection attempts will be made.</li>
 *
 * <li>Connection and disconnection when Chrome is started/stopped: Once a client is registered,
 * it will be disconnected when Chrome goes in the background, and its connection state restored
 * when Chrome comes back to the foreground. That relies on an {@link ApplicationStateListener}.
 * That disconnection can be delayed, call {@link #setDisconnectionDelay(long)} to configure it.
 * </li>
 * </ul>
 *
 * <p>
 * It should be used when we want to keep a Client around for extended durations and use it quite
 * often, independently of the current Activity. For intermittent usage, see {@link ConnectedTask}
 * or directly create {@link GoogleApiClient}s. They are already cheap and resilient by design, and
 * can be tied to a specific activity's lifecycle using
 * {@link GoogleApiClient.Builder#enableAutoManage}.
 * </p>
 *
 * Usage:
 * <pre>
 * {@code
 * // Create a GoogleApiClient as usual
 * GoogleApiClient client = new GoogleApiClient.Builder(context)
 *                                             ...
 *                                             .build();
 *
 * // If further configuration is not needed, you don't need to care about the returned object.
 * GoogleApiClientHelper helper = new GoogleApiClient(client);
 * helper.setDisconnectionDelay(3000);
 *
 * // Use your client as usual.
 * client.connect();
 *
 * ...
 *
 * // If you don't need the client anymore and want to get rid of it, unregister it first. You need
 * // a reference to the GoogleApiClientHelper object for it.
 * helper.disable();
 *
 * // It still has to be disconnected if it's not done already.
 * client.disconnect();
 * }
 * </pre>
 */
public class GoogleApiClientHelper implements OnConnectionFailedListener, ConnectionCallbacks {
    private static final String TAG = "GCore";

    private int mResolutionAttempts;
    private boolean mWasConnectedBefore;
    private final Handler mHandler = new Handler(ThreadUtils.getUiThreadLooper());
    private final GoogleApiClient mClient;
    private long mDisconnectionDelayMs;
    private Runnable mPendingDisconnect;

    /**
     * Creates a helper and enrolls it in the various connection management features.
     * See the class documentation for {@link GoogleApiClientHelper} for more information.
     *
     * @param client The client to wrap.
     */
    public GoogleApiClientHelper(GoogleApiClient client) {
        mClient = client;
        enableConnectionRetrying(true);
        enableLifecycleManagement(true);
    }

    /**
     * Opts in or out of lifecycle management. The client's connection will be closed and reopened
     * when Chrome goes in and out of background.
     *
     * It is safe to set it to the current state. Disabling lifecycle management also cancels
     * pending disconnections.
     */
    public void enableLifecycleManagement(final boolean enabled) {
        Log.d(TAG, "enableLifecycleManagement(%s)", enabled);
        LifecycleHook hook = LifecycleHook.getInstance();

        if (enabled) {
            hook.registerClientHelper(GoogleApiClientHelper.this);
        } else {
            cancelPendingDisconnection();
            hook.unregisterClientHelper(GoogleApiClientHelper.this);
        }
    }

    /**
     * Opts in or out of connection retrying. The client will attempt to connect again after some
     * connection failures.
     *
     * Enabling or disabling it while it is already enabled or disabled has no effect.
     */
    public void enableConnectionRetrying(boolean enabled) {
        if (enabled) {
            mClient.registerConnectionCallbacks(this);
            mClient.registerConnectionFailedListener(this);
        } else {
            mClient.unregisterConnectionCallbacks(this);
            mClient.unregisterConnectionFailedListener(this);
        }
    }

    /**
     * Sets the disconnection delay. It is used to delay disconnection when the lifecycle is
     * managed. That can allow in flight queries to complete before the client is disconnected.
     *
     * The default delay is 0.
     */
    public void setDisconnectionDelay(long delayMs) {
        mDisconnectionDelayMs = delayMs;
    }

    /**
     * Opt out of the various connection management features. This method should be called if the
     * helper features are not desired anymore. It can then be discarded. The client itself can
     * still be used as normal after that.
     */
    public void disable() {
        enableLifecycleManagement(false);
        enableConnectionRetrying(false);
        setDisconnectionDelay(0);
    }

    /**
     * Tells the helper that we are going to use the connection. It should postpone disconnections
     * and make sure the client is connected.
     * This is useful if the client might be used when we are in the background.
     */
    public void willUseConnection() {
        // Cancel and reschedule the disconnection if we are in the background. We do it early to
        // avoid race conditions between a disconnect on the UI thread and the connect below.
        if (!ApplicationStatus.hasVisibleActivities()) scheduleDisconnection();

        // The client might be disconnected if we were idle in the background for too long.
        if (!mClient.isConnected() && !mClient.isConnecting()) {
            Log.d(TAG, "Reconnecting the client.");
            mClient.connect();
        }
    }

    void restoreConnectedState() {
        // If we go back to the foreground before a delayed disconnect happens, cancel it.
        cancelPendingDisconnection();

        if (mWasConnectedBefore) {
            mClient.connect();
        }
    }

    /**
     * Schedule a disconnection of the client after the predefined delay. If there was a
     * disconnection already planned, it will be rescheduled from now.
     */
    void scheduleDisconnection() {
        cancelPendingDisconnection();

        mPendingDisconnect =
                new Runnable() {
                    @Override
                    public void run() {
                        Log.d(TAG, "Disconnect delay expired.");
                        mPendingDisconnect = null;
                        disconnect();
                    }
                };

        mHandler.postDelayed(mPendingDisconnect, mDisconnectionDelayMs);
    }

    private void disconnect() {
        if (mClient.isConnected() || mClient.isConnecting()) {
            mWasConnectedBefore = true;
        }

        // We always call disconnect to abort possibly pending connection requests.
        mClient.disconnect();
    }

    private void cancelPendingDisconnection() {
        if (mPendingDisconnect == null) return;

        mHandler.removeCallbacks(mPendingDisconnect);
        mPendingDisconnect = null;
    }

    @Override
    public void onConnectionFailed(ConnectionResult result) {
        if (!isErrorRecoverableByRetrying(result.getErrorCode())) {
            Log.d(
                    TAG,
                    "Not retrying managed client connection. Unrecoverable error: %d",
                    result.getErrorCode());
            return;
        }

        if (mResolutionAttempts < ConnectedTask.RETRY_NUMBER_LIMIT) {
            Log.d(
                    TAG,
                    "Retrying managed client connection. attempt %d/%d - errorCode: %d",
                    mResolutionAttempts,
                    ConnectedTask.RETRY_NUMBER_LIMIT,
                    result.getErrorCode());
            mResolutionAttempts += 1;

            mHandler.postDelayed(
                    new Runnable() {
                        @Override
                        public void run() {
                            mClient.connect();
                        }
                    },
                    ConnectedTask.CONNECTION_RETRY_TIME_MS);
        }
    }

    @Override
    public void onConnected(Bundle connectionHint) {
        mResolutionAttempts = 0;
    }

    @Override
    public void onConnectionSuspended(int cause) {
        // GoogleApiClient handles retrying on suspension itself. Logging in case it didn't succeed
        // for some reason.
        Log.w(TAG, "Managed client connection suspended. Cause: %d", cause);
    }

    private static boolean isErrorRecoverableByRetrying(int errorCode) {
        return errorCode == ConnectionResult.INTERNAL_ERROR
                || errorCode == ConnectionResult.NETWORK_ERROR
                || errorCode == ConnectionResult.SERVICE_UPDATING;
    }
}