1 // Copyright 2015 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;
6 
7 import android.annotation.SuppressLint;
8 import android.content.Context;
9 import android.net.Uri;
10 import android.os.SystemClock;
11 import android.view.ContextThemeWrapper;
12 import android.view.InflateException;
13 import android.view.View;
14 import android.view.ViewGroup;
15 import android.view.ViewStub;
16 import android.widget.FrameLayout;
17 
18 import androidx.annotation.IntDef;
19 import androidx.annotation.VisibleForTesting;
20 
21 import org.chromium.base.Log;
22 import org.chromium.base.ThreadUtils;
23 import org.chromium.base.TraceEvent;
24 import org.chromium.base.annotations.NativeMethods;
25 import org.chromium.base.library_loader.LibraryLoader;
26 import org.chromium.base.metrics.RecordHistogram;
27 import org.chromium.base.task.AsyncTask;
28 import org.chromium.chrome.R;
29 import org.chromium.chrome.browser.app.ChromeActivity;
30 import org.chromium.chrome.browser.profiles.Profile;
31 import org.chromium.chrome.browser.toolbar.ControlContainer;
32 import org.chromium.components.embedder_support.util.UrlConstants;
33 import org.chromium.content_public.browser.WebContents;
34 import org.chromium.content_public.browser.WebContentsObserver;
35 import org.chromium.ui.LayoutInflaterUtils;
36 
37 import java.lang.annotation.Retention;
38 import java.lang.annotation.RetentionPolicy;
39 import java.net.InetAddress;
40 import java.net.MalformedURLException;
41 import java.net.URL;
42 import java.net.UnknownHostException;
43 import java.util.HashMap;
44 import java.util.HashSet;
45 import java.util.Map;
46 import java.util.Set;
47 
48 /**
49  * This class is a singleton that holds utilities for warming up Chrome and prerendering urls
50  * without creating the Activity.
51  *
52  * This class is not thread-safe and must only be used on the UI thread.
53  */
54 public class WarmupManager {
55     private static final String TAG = "WarmupManager";
56 
57     @VisibleForTesting
58     static final String WEBCONTENTS_STATUS_HISTOGRAM = "CustomTabs.SpareWebContents.Status2";
59 
60     public static final boolean FOR_CCT = true;
61 
62     // See CustomTabs.SpareWebContentsStatus histogram. Append-only.
63     @IntDef({WebContentsStatus.CREATED, WebContentsStatus.USED, WebContentsStatus.KILLED,
64             WebContentsStatus.DESTROYED, WebContentsStatus.STOLEN})
65     @Retention(RetentionPolicy.SOURCE)
66     @interface WebContentsStatus {
67         @VisibleForTesting
68         int CREATED = 0;
69         @VisibleForTesting
70         int USED = 1;
71         @VisibleForTesting
72         int KILLED = 2;
73         @VisibleForTesting
74         int DESTROYED = 3;
75         @VisibleForTesting
76         int STOLEN = 4;
77         int NUM_ENTRIES = 5;
78     }
79 
80     /**
81      * Observes spare WebContents deaths. In case of death, records stats, and cleanup the objects.
82      */
83     private class RenderProcessGoneObserver extends WebContentsObserver {
84         @Override
renderProcessGone(boolean wasOomProtected)85         public void renderProcessGone(boolean wasOomProtected) {
86             long elapsed = SystemClock.elapsedRealtime() - mWebContentsCreationTimeMs;
87             RecordHistogram.recordLongTimesHistogram(
88                     "CustomTabs.SpareWebContents.TimeBeforeDeath", elapsed);
89             recordWebContentsStatus(WebContentsStatus.KILLED);
90             destroySpareWebContentsInternal();
91         }
92     }
93 
94     @SuppressLint("StaticFieldLeak")
95     private static WarmupManager sWarmupManager;
96 
97     private final Set<String> mDnsRequestsInFlight;
98     private final Map<String, Profile> mPendingPreconnectWithProfile;
99 
100     private int mToolbarContainerId;
101     private ViewGroup mMainView;
102     @VisibleForTesting
103     WebContents mSpareWebContents;
104     private long mWebContentsCreationTimeMs;
105     private RenderProcessGoneObserver mObserver;
106     private boolean mWebContentsCreatedForCCT;
107 
108     /**
109      * @return The singleton instance for the WarmupManager, creating one if necessary.
110      */
getInstance()111     public static WarmupManager getInstance() {
112         ThreadUtils.assertOnUiThread();
113         if (sWarmupManager == null) sWarmupManager = new WarmupManager();
114         return sWarmupManager;
115     }
116 
WarmupManager()117     private WarmupManager() {
118         mDnsRequestsInFlight = new HashSet<>();
119         mPendingPreconnectWithProfile = new HashMap<>();
120     }
121 
122     /**
123      * Inflates and constructs the view hierarchy that the app will use.
124      * @param baseContext The base context to use for creating the ContextWrapper.
125      * @param toolbarContainerId Id of the toolbar container.
126      * @param toolbarId The toolbar's layout ID.
127      */
initializeViewHierarchy(Context baseContext, int toolbarContainerId, int toolbarId)128     public void initializeViewHierarchy(Context baseContext, int toolbarContainerId,
129             int toolbarId) {
130         ThreadUtils.assertOnUiThread();
131         if (mMainView != null && mToolbarContainerId == toolbarContainerId) return;
132         mMainView = inflateViewHierarchy(baseContext, toolbarContainerId, toolbarId);
133         mToolbarContainerId = toolbarContainerId;
134     }
135 
136     /**
137      * Inflates and constructs the view hierarchy that the app will use.
138      * Calls to this are not restricted to the UI thread.
139      * @param baseContext The base context to use for creating the ContextWrapper.
140      * @param toolbarContainerId Id of the toolbar container.
141      * @param toolbarId The toolbar's layout ID.
142      */
inflateViewHierarchy( Context baseContext, int toolbarContainerId, int toolbarId)143     public static ViewGroup inflateViewHierarchy(
144             Context baseContext, int toolbarContainerId, int toolbarId) {
145         try (TraceEvent e = TraceEvent.scoped("WarmupManager.inflateViewHierarchy")) {
146             ContextThemeWrapper context =
147                     new ContextThemeWrapper(baseContext, ChromeActivity.getThemeId());
148             FrameLayout contentHolder = new FrameLayout(context);
149             ViewGroup mainView =
150                     (ViewGroup) LayoutInflaterUtils.inflate(context, R.layout.main, contentHolder);
151             if (toolbarContainerId != ChromeActivity.NO_CONTROL_CONTAINER) {
152                 ViewStub stub = (ViewStub) mainView.findViewById(R.id.control_container_stub);
153                 stub.setLayoutResource(toolbarContainerId);
154                 stub.inflate();
155             }
156             // It cannot be assumed that the result of toolbarContainerStub.inflate() will be
157             // the control container since it may be wrapped in another view.
158             ControlContainer controlContainer =
159                     (ControlContainer) mainView.findViewById(R.id.control_container);
160 
161             if (toolbarId != ChromeActivity.NO_TOOLBAR_LAYOUT && controlContainer != null) {
162                 controlContainer.initWithToolbar(toolbarId);
163             }
164             return mainView;
165         } catch (InflateException e) {
166             // See https://crbug.com/606715.
167             Log.e(TAG, "Inflation exception.", e);
168             return null;
169         }
170     }
171 
172     /**
173      * Transfers all the children in the local view hierarchy {@link #mMainView} to the given
174      * ViewGroup {@param contentView} as child.
175      * @param contentView The parent ViewGroup to use for the transfer.
176      */
transferViewHierarchyTo(ViewGroup contentView)177     public void transferViewHierarchyTo(ViewGroup contentView) {
178         ThreadUtils.assertOnUiThread();
179         ViewGroup viewHierarchy = mMainView;
180         mMainView = null;
181         if (viewHierarchy == null) return;
182         transferViewHeirarchy(viewHierarchy, contentView);
183     }
184 
185     /**
186      * Transfers all the children in one view hierarchy {@param from} to another {@param to}.
187      * @param from The parent ViewGroup to transfer children from.
188      * @param to The parent ViewGroup to transfer children to.
189      */
transferViewHeirarchy(ViewGroup from, ViewGroup to)190     public static void transferViewHeirarchy(ViewGroup from, ViewGroup to) {
191         while (from.getChildCount() > 0) {
192             View currentChild = from.getChildAt(0);
193             from.removeView(currentChild);
194             to.addView(currentChild);
195         }
196     }
197 
198     /**
199      * @return Whether a pre-built view hierarchy exists for the given toolbarContainerId.
200      */
hasViewHierarchyWithToolbar(int toolbarContainerId)201     public boolean hasViewHierarchyWithToolbar(int toolbarContainerId) {
202         ThreadUtils.assertOnUiThread();
203         return mMainView != null && mToolbarContainerId == toolbarContainerId;
204     }
205 
206     /**
207      * Clears the inflated view hierarchy.
208      */
clearViewHierarchy()209     public void clearViewHierarchy() {
210         ThreadUtils.assertOnUiThread();
211         mMainView = null;
212     }
213 
214     /**
215      * Launches a background DNS query for a given URL.
216      *
217      * @param url URL from which the domain to query is extracted.
218      */
prefetchDnsForUrlInBackground(final String url)219     private void prefetchDnsForUrlInBackground(final String url) {
220         mDnsRequestsInFlight.add(url);
221         new AsyncTask<Void>() {
222             @Override
223             protected Void doInBackground() {
224                 try (TraceEvent e =
225                                 TraceEvent.scoped("WarmupManager.prefetchDnsForUrlInBackground")) {
226                     InetAddress.getByName(new URL(url).getHost());
227                 } catch (MalformedURLException e) {
228                     // We don't do anything with the result of the request, it
229                     // is only here to warm up the cache, thus ignoring the
230                     // exception is fine.
231                 } catch (UnknownHostException e) {
232                     // As above.
233                 }
234                 return null;
235             }
236 
237             @Override
238             protected void onPostExecute(Void result) {
239                 mDnsRequestsInFlight.remove(url);
240                 if (mPendingPreconnectWithProfile.containsKey(url)) {
241                     Profile profile = mPendingPreconnectWithProfile.get(url);
242                     mPendingPreconnectWithProfile.remove(url);
243                     maybePreconnectUrlAndSubResources(profile, url);
244                 }
245             }
246         }
247                 .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
248     }
249 
250     /** Launches a background DNS query for a given URL.
251      *
252      * @param context The Application context.
253      * @param url URL from which the domain to query is extracted.
254      */
maybePrefetchDnsForUrlInBackground(Context context, String url)255     public void maybePrefetchDnsForUrlInBackground(Context context, String url) {
256         ThreadUtils.assertOnUiThread();
257             prefetchDnsForUrlInBackground(url);
258     }
259 
260     /**
261      * Starts asynchronous initialization of the preconnect predictor.
262      *
263      * Without this call, |maybePreconnectUrlAndSubresources()| will not use a database of origins
264      * to connect to, unless the predictor has already been initialized in another way.
265      *
266      * @param profile The profile to use for the predictor.
267      */
startPreconnectPredictorInitialization(Profile profile)268     public static void startPreconnectPredictorInitialization(Profile profile) {
269         ThreadUtils.assertOnUiThread();
270         WarmupManagerJni.get().startPreconnectPredictorInitialization(profile);
271     }
272 
273     /**
274      * Reports to WarmupManager on the next set of URLs that the user is expected to navigate to
275      * next. The set of URLs are reported by an external Android app.
276      *
277      * @param profile The profile to use.
278      * @param packagesName Possible names of the external Android apps that may have reported the
279      *         set of URLs.
280      * @param urls Ordered list of URLs that the user is expected to navigate to next. The URLs are
281      *         ordered in non-increasing probability of navigation.
282      */
reportNextLikelyNavigationsOnUiThread( Profile profile, String[] packagesName, String[] urls)283     public static void reportNextLikelyNavigationsOnUiThread(
284             Profile profile, String[] packagesName, String[] urls) {
285         ThreadUtils.assertOnUiThread();
286         WarmupManagerJni.get().reportNextLikelyNavigations(profile, packagesName, urls);
287     }
288 
289     /** Asynchronously preconnects to a given URL if the data reduction proxy is not in use.
290      *
291      * @param profile The profile to use for the preconnection.
292      * @param url The URL we want to preconnect to.
293      */
maybePreconnectUrlAndSubResources(Profile profile, String url)294     public void maybePreconnectUrlAndSubResources(Profile profile, String url) {
295         ThreadUtils.assertOnUiThread();
296 
297         Uri uri = Uri.parse(url);
298         if (uri == null) return;
299         String scheme = uri.normalizeScheme().getScheme();
300         if (!UrlConstants.HTTP_SCHEME.equals(scheme) && !UrlConstants.HTTPS_SCHEME.equals(scheme)) {
301             return;
302         }
303 
304         // If there is already a DNS request in flight for this URL, then the preconnection will
305         // start by issuing a DNS request for the same domain, as the result is not cached. However,
306         // such a DNS request has already been sent from this class, so it is better to wait for the
307         // answer to come back before preconnecting. Otherwise, the preconnection logic will wait
308         // for the result of the second DNS request, which should arrive after the result of the
309         // first one. Note that we however need to wait for the main thread to be available in this
310         // case, since the preconnection will be sent from AsyncTask.onPostExecute(), which may
311         // delay it.
312         if (mDnsRequestsInFlight.contains(url)) {
313             // Note that if two requests come for the same URL with two different profiles, the last
314             // one will win.
315             mPendingPreconnectWithProfile.put(url, profile);
316         } else {
317             WarmupManagerJni.get().preconnectUrlAndSubresources(profile, url);
318         }
319     }
320 
321     /**
322      * Warms up a spare, empty RenderProcessHost that may be used for subsequent navigations.
323      *
324      * The spare RenderProcessHost will be used automatically in subsequent navigations.
325      * There is nothing further the WarmupManager needs to do to enable that use.
326      *
327      * This uses a different mechanism than createSpareWebContents, below, and is subject
328      * to fewer restrictions.
329      *
330      * This must be called from the UI thread.
331      */
createSpareRenderProcessHost(Profile profile)332     public void createSpareRenderProcessHost(Profile profile) {
333         ThreadUtils.assertOnUiThread();
334         if (!LibraryLoader.getInstance().isInitialized()) return;
335 
336         destroySpareWebContents();
337         WarmupManagerJni.get().warmupSpareRenderer(profile);
338     }
339 
340     /**
341      * Creates and initializes a spare WebContents, to be used in a subsequent navigation.
342      *
343      * This creates a renderer that is suitable for any navigation. It can be picked up by any tab.
344      * Can be called multiple times, and must be called from the UI thread.
345      *
346      * @param forCCT Whether this WebContents is being created for CCT.
347      */
createSpareWebContents(boolean forCCT)348     public void createSpareWebContents(boolean forCCT) {
349         ThreadUtils.assertOnUiThread();
350         if (!LibraryLoader.getInstance().isInitialized() || mSpareWebContents != null) return;
351 
352         mWebContentsCreatedForCCT = forCCT;
353         mSpareWebContents = new WebContentsFactory().createWebContentsWithWarmRenderer(
354                 Profile.getLastUsedRegularProfile(), true /* initiallyHidden */);
355         mObserver = new RenderProcessGoneObserver();
356         mSpareWebContents.addObserver(mObserver);
357         mWebContentsCreationTimeMs = SystemClock.elapsedRealtime();
358         recordWebContentsStatus(WebContentsStatus.CREATED);
359     }
360 
361     /**
362      * Destroys the spare WebContents if there is one.
363      */
destroySpareWebContents()364     public void destroySpareWebContents() {
365         ThreadUtils.assertOnUiThread();
366         if (mSpareWebContents == null) return;
367         recordWebContentsStatus(WebContentsStatus.DESTROYED);
368         destroySpareWebContentsInternal();
369     }
370 
371     /**
372      * Returns a spare WebContents or null, depending on the availability of one.
373      *
374      * The parameters are the same as for {@link WebContentsFactory#createWebContents()}.
375      * @param forCCT Whether this WebContents is being taken by CCT.
376      *
377      * @return a WebContents, or null.
378      */
takeSpareWebContents( boolean incognito, boolean initiallyHidden, boolean forCCT)379     public WebContents takeSpareWebContents(
380             boolean incognito, boolean initiallyHidden, boolean forCCT) {
381         ThreadUtils.assertOnUiThread();
382         if (incognito) return null;
383         WebContents result = mSpareWebContents;
384         if (result == null) return null;
385         mSpareWebContents = null;
386         result.removeObserver(mObserver);
387         mObserver = null;
388         if (!initiallyHidden) result.onShow();
389         recordWebContentsStatus(mWebContentsCreatedForCCT == forCCT ? WebContentsStatus.USED
390                                                                     : WebContentsStatus.STOLEN);
391         return result;
392     }
393 
394     /**
395      * @return Whether a spare renderer is available.
396      */
hasSpareWebContents()397     public boolean hasSpareWebContents() {
398         return mSpareWebContents != null;
399     }
400 
destroySpareWebContentsInternal()401     private void destroySpareWebContentsInternal() {
402         mSpareWebContents.removeObserver(mObserver);
403         mSpareWebContents.destroy();
404         mSpareWebContents = null;
405         mObserver = null;
406     }
407 
recordWebContentsStatus(@ebContentsStatus int status)408     private void recordWebContentsStatus(@WebContentsStatus int status) {
409         if (!mWebContentsCreatedForCCT) return;
410         RecordHistogram.recordEnumeratedHistogram(
411                 WEBCONTENTS_STATUS_HISTOGRAM, status, WebContentsStatus.NUM_ENTRIES);
412     }
413 
414     @NativeMethods
415     interface Natives {
startPreconnectPredictorInitialization(Profile profile)416         void startPreconnectPredictorInitialization(Profile profile);
preconnectUrlAndSubresources(Profile profile, String url)417         void preconnectUrlAndSubresources(Profile profile, String url);
warmupSpareRenderer(Profile profile)418         void warmupSpareRenderer(Profile profile);
reportNextLikelyNavigations(Profile profile, String[] packagesName, String[] urls)419         void reportNextLikelyNavigations(Profile profile, String[] packagesName, String[] urls);
420     }
421 }
422