// Copyright 2015 The Chromium 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 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 = 0; private boolean mWasConnectedBefore = false; private final Handler mHandler = new Handler(ThreadUtils.getUiThreadLooper()); private final GoogleApiClient mClient; private long mDisconnectionDelayMs = 0; 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; } }