chromium/chrome/android/java/src/org/chromium/chrome/browser/customtabs/NavigationInfoCaptureTrigger.java

// Copyright 2018 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.customtabs;

import android.os.Handler;

import org.chromium.base.Callback;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.tab.Tab;

import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;

/**
 * This class contains logic for capturing navigation info at an appropriate time.
 *
 * We want to capture navigation information after both onload and first meaningful paint have
 * triggered. We add a slight delay to avoid capturing during CPU intensive periods.
 *
 * If a capture has not been taken after a long amount of time or when the Tab is hidden, we also
 * capture.
 */
public class NavigationInfoCaptureTrigger {
    private static final int ONLOAD_DELAY_MS = 1000;
    private static final int ONLOAD_LONG_DELAY_MS = 15000;
    private static final int ONHIDE_DELAY_MS = 1000;
    private static final int FMP_DELAY_MS = 3000;

    private final Callback<Tab> mCapture;
    private final Handler mUiThreadHandler = new Handler(ThreadUtils.getUiThreadLooper());
    private final List<Runnable> mPendingRunnables = new LinkedList<>();

    private boolean mOnloadTriggered;
    private boolean mFirstMeaningfulPaintTriggered;
    private boolean mCaptureTaken;

    public NavigationInfoCaptureTrigger(Callback<Tab> capture) {
        mCapture = capture;
    }

    /** Notifies that a page navigation has occurred and state should reset. */
    public void onNewNavigation() {
        mOnloadTriggered = false;
        mFirstMeaningfulPaintTriggered = false;
        mCaptureTaken = false;
        clearPendingRunnables();
    }

    /** Notifies that onload has occurred. */
    public void onLoadFinished(Tab tab) {
        mOnloadTriggered = true;
        captureDelayedIf(tab, () -> mFirstMeaningfulPaintTriggered, ONLOAD_DELAY_MS);
        captureDelayed(tab, ONLOAD_LONG_DELAY_MS);
    }

    /** Notifies that first meaningful paint has occurred. */
    public void onFirstMeaningfulPaint(Tab tab) {
        mFirstMeaningfulPaintTriggered = true;
        captureDelayedIf(tab, () -> mOnloadTriggered, FMP_DELAY_MS);
    }

    /** Notifies that the Tab has been hidden. */
    public void onHide(Tab tab) {
        captureDelayed(tab, ONHIDE_DELAY_MS);
    }

    private void clearPendingRunnables() {
        for (Runnable pendingRunnable : mPendingRunnables) {
            mUiThreadHandler.removeCallbacks(pendingRunnable);
        }
        mPendingRunnables.clear();
    }

    /** Posts a CaptureRunnable that will capture navigation info after the delay (ms). */
    private void captureDelayed(Tab tab, long delay) {
        captureDelayedIf(tab, () -> true, delay);
    }

    /**
     * Posts a CaptureRunnable that will capture navigation info after the delay (ms) if the check
     * passes.
     */
    private void captureDelayedIf(Tab tab, Callable<Boolean> check, long delay) {
        if (mCaptureTaken) return;
        Runnable runnable = new CaptureRunnable(tab, check);
        mPendingRunnables.add(runnable);
        mUiThreadHandler.postDelayed(runnable, delay);
    }

    /**
     * A Runnable that when executes ensures that no capture has already been taken and that the
     * check passes, then captures the navigation info and clears all other pending Runnables.
     */
    private class CaptureRunnable implements Runnable {
        private final Callable<Boolean> mCheck;
        private final Tab mTab;

        public CaptureRunnable(Tab tab, Callable<Boolean> check) {
            mCheck = check;
            mTab = tab;
        }

        @Override
        public void run() {
            assert !mCaptureTaken;

            try {
                if (!mCheck.call()) return;
            } catch (Exception e) {
                // In case mCheck.call throws an exception (which it shouldn't, but it's part of
                // Callable#call's signature).
                throw new RuntimeException(e);
            }

            mCapture.onResult(mTab);
            mCaptureTaken = true;

            clearPendingRunnables();
        }
    }
}