// Copyright 2015 The Chromium Authors. All rights reserved.
// 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;
import android.content.Context;
import android.os.AsyncTask;
import android.os.StrictMode;
import android.view.ContextThemeWrapper;
import android.view.InflateException;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.FrameLayout;
import org.chromium.base.Log;
import org.chromium.base.SysUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.net.spdyproxy.DataReductionProxySettings;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.content_public.browser.WebContents;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* This class is a singleton that holds utilities for warming up Chrome and prerendering urls
* without creating the Activity.
*
* This class is not thread-safe and must only be used on the UI thread.
*/
public final class WarmupManager {
private static final String TAG = "WarmupManager";
private static WarmupManager sWarmupManager;
private final Set<String> mDnsRequestsInFlight;
private final Map<String, Profile> mPendingPreconnectWithProfile;
private int mToolbarContainerId;
private ViewGroup mMainView;
private WebContents mSpareWebContents;
/**
* @return The singleton instance for the WarmupManager, creating one if necessary.
*/
public static WarmupManager getInstance() {
ThreadUtils.assertOnUiThread();
if (sWarmupManager == null) sWarmupManager = new WarmupManager();
return sWarmupManager;
}
private WarmupManager() {
mDnsRequestsInFlight = new HashSet<String>();
mPendingPreconnectWithProfile = new HashMap<String, Profile>();
}
/**
* Inflates and constructs the view hierarchy that the app will use.
* @param baseContext The base context to use for creating the ContextWrapper.
* @param toolbarContainerId Id of the toolbar container.
*/
public void initializeViewHierarchy(Context baseContext, int toolbarContainerId) {
TraceEvent.begin("WarmupManager.initializeViewHierarchy");
// Inflating the view hierarchy causes StrictMode violations on some
// devices. Since layout inflation should happen on the UI thread, allow
// the disk reads. crbug.com/644243.
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
ThreadUtils.assertOnUiThread();
if (mMainView != null && mToolbarContainerId == toolbarContainerId) return;
ContextThemeWrapper context =
new ContextThemeWrapper(baseContext, ChromeActivity.getThemeId());
FrameLayout contentHolder = new FrameLayout(context);
mMainView = (ViewGroup) LayoutInflater.from(context).inflate(
R.layout.main, contentHolder);
mToolbarContainerId = toolbarContainerId;
if (toolbarContainerId != ChromeActivity.NO_CONTROL_CONTAINER) {
ViewStub stub = (ViewStub) mMainView.findViewById(R.id.control_container_stub);
stub.setLayoutResource(toolbarContainerId);
stub.inflate();
}
} catch (InflateException e) {
// See crbug.com/606715.
Log.e(TAG, "Inflation exception.", e);
mMainView = null;
} finally {
StrictMode.setThreadPolicy(oldPolicy);
TraceEvent.end("WarmupManager.initializeViewHierarchy");
}
}
/**
* Transfers all the children in the view hierarchy to the giving ViewGroup as child.
* @param contentView The parent ViewGroup to use for the transfer.
*/
public void transferViewHierarchyTo(ViewGroup contentView) {
ThreadUtils.assertOnUiThread();
ViewGroup viewHierarchy = mMainView;
mMainView = null;
if (viewHierarchy == null) return;
while (viewHierarchy.getChildCount() > 0) {
View currentChild = viewHierarchy.getChildAt(0);
viewHierarchy.removeView(currentChild);
contentView.addView(currentChild);
}
}
/**
* @return Whether the view hierarchy has been prebuilt with a given toolbar ID. If there is no
* match, clears the inflated view.
*/
public boolean hasBuiltOrClearViewHierarchyWithToolbar(int toolbarContainerId) {
ThreadUtils.assertOnUiThread();
boolean match = mMainView != null && mToolbarContainerId == toolbarContainerId;
if (!match) mMainView = null;
return match;
}
/**
* Launches a background DNS query for a given URL.
*
* @param url URL from which the domain to query is extracted.
*/
private void prefetchDnsForUrlInBackground(final String url) {
mDnsRequestsInFlight.add(url);
new AsyncTask<String, Void, Void>() {
@Override
protected Void doInBackground(String... params) {
try {
InetAddress.getByName(new URL(url).getHost());
} catch (MalformedURLException e) {
// We don't do anything with the result of the request, it
// is only here to warm up the cache, thus ignoring the
// exception is fine.
} catch (UnknownHostException e) {
// As above.
}
return null;
}
@Override
protected void onPostExecute(Void result) {
mDnsRequestsInFlight.remove(url);
if (mPendingPreconnectWithProfile.containsKey(url)) {
Profile profile = mPendingPreconnectWithProfile.get(url);
mPendingPreconnectWithProfile.remove(url);
maybePreconnectUrlAndSubResources(profile, url);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, url);
}
/** Launches a background DNS query for a given URL if the data reduction proxy is not in use.
*
* @param context The Application context.
* @param url URL from which the domain to query is extracted.
*/
public void maybePrefetchDnsForUrlInBackground(Context context, String url) {
ThreadUtils.assertOnUiThread();
if (!DataReductionProxySettings.isEnabledBeforeNativeLoad(context)) {
prefetchDnsForUrlInBackground(url);
}
}
/** Asynchronously preconnects to a given URL if the data reduction proxy is not in use.
*
* @param profile The profile to use for the preconnection.
* @param url The URL we want to preconnect to.
*/
public void maybePreconnectUrlAndSubResources(Profile profile, String url) {
ThreadUtils.assertOnUiThread();
if (!DataReductionProxySettings.getInstance().isDataReductionProxyEnabled()) {
// If there is already a DNS request in flight for this URL, then
// the preconnection will start by issuing a DNS request for the
// same domain, as the result is not cached. However, such a DNS
// request has already been sent from this class, so it is better to
// wait for the answer to come back before preconnecting. Otherwise,
// the preconnection logic will wait for the result of the second
// DNS request, which should arrive after the result of the first
// one. Note that we however need to wait for the main thread to be
// available in this case, since the preconnection will be sent from
// AsyncTask.onPostExecute(), which may delay it.
if (mDnsRequestsInFlight.contains(url)) {
// Note that if two requests come for the same URL with two
// different profiles, the last one will win.
mPendingPreconnectWithProfile.put(url, profile);
} else {
nativePreconnectUrlAndSubresources(profile, url);
}
}
}
/**
* Creates and initializes a spare WebContents, to be used in a subsequent navigation.
*
* This creates a renderer that is suitable for any navigation. It can be picked up by any tab.
* Can be called multiple times, and must be called from the UI thread.
* Note that this is a no-op on low-end devices.
*/
public void createSpareWebContents() {
ThreadUtils.assertOnUiThread();
if (mSpareWebContents != null || SysUtils.isLowEndDevice()) return;
mSpareWebContents = WebContentsFactory.createWebContentsWithWarmRenderer(false, false);
}
/**
* Destroys the spare WebContents if there is one.
*/
public void destroySpareWebContents() {
ThreadUtils.assertOnUiThread();
if (mSpareWebContents == null) return;
mSpareWebContents.destroy();
mSpareWebContents = null;
}
/**
* Returns a spare WebContents or null, depending on the availability of one.
*
* The parameters are the same as for {@link WebContentsFactory#createWebContents()}.
*
* @return a WebContents, or null.
*/
public WebContents takeSpareWebContents(boolean incognito, boolean initiallyHidden) {
ThreadUtils.assertOnUiThread();
if (incognito || initiallyHidden) return null;
WebContents result = mSpareWebContents;
mSpareWebContents = null;
return result;
}
/**
* @return Whether a spare renderer is available.
*/
public boolean hasSpareWebContents() {
return mSpareWebContents != null;
}
private static native void nativePreconnectUrlAndSubresources(Profile profile, String url);
}