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