chromium/chrome/android/java/src/org/chromium/chrome/browser/gcore/ConnectedTask.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.text.format.DateUtils;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.Log;
import org.chromium.base.TraceEvent;
import org.chromium.build.annotations.CheckDiscard;

/**
 * Base class for tasks which connects to Google Play Services using given GoogleApiClient,
 * performs action specified in doWhenConnected method, disconnects the client and cleans up
 * by invoking cleanUp method.
 *
 * <p>
 * Using the same client for tasks running in more than one thread is a serious error, as
 * the state can then be modified while other threads are still using the client. The
 * recommended way to use these tasks is with a {@link java.util.concurrent.ThreadPoolExecutor}
 * having a pool size of 1.
 * </p>
 * <p>
 * This class waits {@link #CONNECTION_TIMEOUT_MS} milliseconds for connection to be established.
 * If connection is unsuccessful then it will retry after {@link #CONNECTION_RETRY_TIME_MS}
 * milliseconds as long as Google Play Services is available. Number of retries is limited to
 * {@link #RETRY_NUMBER_LIMIT}.
 * </p>
 *
 * @param <T> type of {@link ChromeGoogleApiClient} to use for the tasks
 */
public abstract class ConnectedTask<T extends ChromeGoogleApiClient> implements Runnable {
    private static final String TAG = "GCore";

    public static final long CONNECTION_TIMEOUT_MS = DateUtils.SECOND_IN_MILLIS * 5;
    public static final long CONNECTION_RETRY_TIME_MS = DateUtils.SECOND_IN_MILLIS * 10;
    public static final int RETRY_NUMBER_LIMIT = 5;

    private final T mClient;
    private int mRetryNumber;

    /**
     * Used for logging and tracing.
     * <ul>
     * <li>Log format: "{logPrefix}| {{@link #getName()}} {message}"</li>
     * <li>Trace format: "ConnectedTask:{logPrefix}:{traceEventName}"</li>
     * </ul>
     */
    private final String mLogPrefix;

    /**
     * @param client
     * @param logPrefix used for logging and tracing. Must be string literal.
     */
    public ConnectedTask(T client, String logPrefix) {
        assert logPrefix != null;
        mClient = client;
        mLogPrefix = logPrefix;
    }

    /** Creates a connected task with an empty log prefix. */
    @VisibleForTesting
    public ConnectedTask(T client) {
        this(client, "");
    }

    /**
     * Executed with client connected to Google Play Services.
     * This method is intended to be overridden by a subclass.
     */
    protected abstract void doWhenConnected(T client);

    /** Returns a name of a task (for debug logging). */
    @CheckDiscard("getName() is only for debug logging")
    protected abstract String getName();

    /**
     * Executed after doWhenConnected was done and client was disconnected.
     * May also be executed when Google Play Services is no longer available, which means connection
     * was unsuccessful and won't be retried.
     * This method is intended to be overridden by a subclass.
     */
    protected void cleanUp() {}

    /**
     * Executed if the connection was unsuccessful.
     * This method is intended to be overridden by a subclass.
     */
    protected void connectionFailed() {}

    private void debugLog(String message) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "%s:%s %s", mLogPrefix, getName(), message);
        }
    }

    @Override
    @VisibleForTesting
    // We always only pass in a string literal here.
    @SuppressWarnings("NoDynamicStringsInTraceEventCheck")
    public final void run() {
        TraceEvent.begin("GCore:" + mLogPrefix + ":run");
        try {
            debugLog("started");
            if (mClient.connectWithTimeout(CONNECTION_TIMEOUT_MS)) {
                try {
                    debugLog("connected");
                    doWhenConnected(mClient);
                    debugLog("finished");
                } finally {
                    mClient.disconnect();
                    debugLog("disconnected");
                    cleanUp();
                    debugLog("cleaned up");
                }
            } else {
                mRetryNumber++;
                if (mRetryNumber < RETRY_NUMBER_LIMIT && mClient.isGooglePlayServicesAvailable()) {
                    debugLog("calling retry");
                    retry(this, CONNECTION_RETRY_TIME_MS);
                } else {
                    connectionFailed();
                    debugLog("number of retries exceeded");
                    cleanUp();
                    debugLog("cleaned up");
                }
            }
        } finally {
            TraceEvent.end("GCore:" + mLogPrefix + ":run");
        }
    }

    /** Method to implement to determine how to run the retry task. */
    protected abstract void retry(Runnable task, long delayMs);
}