1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.customtabs; 6 7 import android.os.Handler; 8 9 import org.chromium.base.Callback; 10 import org.chromium.base.ThreadUtils; 11 import org.chromium.chrome.browser.tab.Tab; 12 13 import java.util.LinkedList; 14 import java.util.List; 15 import java.util.concurrent.Callable; 16 17 /** 18 * This class contains logic for capturing navigation info at an appropriate time. 19 * 20 * We want to capture navigation information after both onload and first meaningful paint have 21 * triggered. We add a slight delay to avoid capturing during CPU intensive periods. 22 * 23 * If a capture has not been taken after a long amount of time or when the Tab is hidden, we also 24 * capture. 25 */ 26 public class NavigationInfoCaptureTrigger { 27 private static final int ONLOAD_DELAY_MS = 1000; 28 private static final int ONLOAD_LONG_DELAY_MS = 15000; 29 private static final int ONHIDE_DELAY_MS = 1000; 30 private static final int FMP_DELAY_MS = 3000; 31 32 private final Callback<Tab> mCapture; 33 private final Handler mUiThreadHandler = new Handler(ThreadUtils.getUiThreadLooper()); 34 private final List<Runnable> mPendingRunnables = new LinkedList<>(); 35 36 private boolean mOnloadTriggered; 37 private boolean mFirstMeaningfulPaintTriggered; 38 private boolean mCaptureTaken; 39 NavigationInfoCaptureTrigger(Callback<Tab> capture)40 public NavigationInfoCaptureTrigger(Callback<Tab> capture) { 41 mCapture = capture; 42 } 43 44 /** Notifies that a page navigation has occurred and state should reset. */ onNewNavigation()45 public void onNewNavigation() { 46 mOnloadTriggered = false; 47 mFirstMeaningfulPaintTriggered = false; 48 mCaptureTaken = false; 49 clearPendingRunnables(); 50 } 51 52 /** Notifies that onload has occurred. */ onLoadFinished(Tab tab)53 public void onLoadFinished(Tab tab) { 54 mOnloadTriggered = true; 55 captureDelayedIf(tab, () -> mFirstMeaningfulPaintTriggered, ONLOAD_DELAY_MS); 56 captureDelayed(tab, ONLOAD_LONG_DELAY_MS); 57 } 58 59 /** Notifies that first meaningful paint has occurred. */ onFirstMeaningfulPaint(Tab tab)60 public void onFirstMeaningfulPaint(Tab tab) { 61 mFirstMeaningfulPaintTriggered = true; 62 captureDelayedIf(tab, () -> mOnloadTriggered, FMP_DELAY_MS); 63 } 64 65 /** Notifies that the Tab has been hidden. */ onHide(Tab tab)66 public void onHide(Tab tab) { 67 captureDelayed(tab, ONHIDE_DELAY_MS); 68 } 69 clearPendingRunnables()70 private void clearPendingRunnables() { 71 for (Runnable pendingRunnable: mPendingRunnables) { 72 mUiThreadHandler.removeCallbacks(pendingRunnable); 73 } 74 mPendingRunnables.clear(); 75 } 76 77 /** Posts a CaptureRunnable that will capture navigation info after the delay (ms). */ captureDelayed(Tab tab, long delay)78 private void captureDelayed(Tab tab, long delay) { 79 captureDelayedIf(tab, () -> true, delay); 80 } 81 82 /** 83 * Posts a CaptureRunnable that will capture navigation info after the delay (ms) if the check 84 * passes. 85 */ captureDelayedIf(Tab tab, Callable<Boolean> check, long delay)86 private void captureDelayedIf(Tab tab, Callable<Boolean> check, long delay) { 87 if (mCaptureTaken) return; 88 Runnable runnable = new CaptureRunnable(tab, check); 89 mPendingRunnables.add(runnable); 90 mUiThreadHandler.postDelayed(runnable, delay); 91 } 92 93 /** 94 * A Runnable that when executes ensures that no capture has already been taken and that the 95 * check passes, then captures the navigation info and clears all other pending Runnables. 96 */ 97 private class CaptureRunnable implements Runnable { 98 private final Callable<Boolean> mCheck; 99 private final Tab mTab; 100 CaptureRunnable(Tab tab, Callable<Boolean> check)101 public CaptureRunnable(Tab tab, Callable<Boolean> check) { 102 mCheck = check; 103 mTab = tab; 104 } 105 106 @Override run()107 public void run() { 108 assert !mCaptureTaken; 109 110 try { 111 if (!mCheck.call()) return; 112 } catch (Exception e) { 113 // In case mCheck.call throws an exception (which it shouldn't, but it's part of 114 // Callable#call's signature). 115 throw new RuntimeException(e); 116 } 117 118 mCapture.onResult(mTab); 119 mCaptureTaken = true; 120 121 clearPendingRunnables(); 122 } 123 } 124 } 125