Preloading support in browser

	   Apps like the QSB can request the browser to preload a
	   web page.
           - preloaded pages are not added to the browser history
	     if they'r not seen by the user
	   - when a request is received, a new tab is created for the
	     preloaded page, but not added to the tab list
 	   - upon receiving the view intent for the preloaded page
             the tab is added to the tab list, and shown
	   - if several pages are preloaded consecutively in the same tab,
             the back stack is cleared before it is displayed
           - preloaded pages use the main browser cookie jar, so pages that
             have never been viewed by the user can drop cookies

Change-Id: I9ed21f2c9560fda0ed042b460b73bb33988a2e8a
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java
index d364378..1836e6e 100644
--- a/src/com/android/browser/BaseUi.java
+++ b/src/com/android/browser/BaseUi.java
@@ -60,7 +60,7 @@
 /**
  * UI interface definitions
  */
-public abstract class BaseUi implements UI, WebViewFactory, OnTouchListener {
+public abstract class BaseUi implements UI, OnTouchListener {
 
     private static final String LOGTAG = "BaseUi";
 
@@ -145,41 +145,6 @@
                 config.getScaledTouchSlop());
     }
 
-    @Override
-    public WebView createWebView(boolean privateBrowsing) {
-        // Create a new WebView
-        BrowserWebView w = new BrowserWebView(mActivity, null,
-                android.R.attr.webViewStyle, privateBrowsing);
-        initWebViewSettings(w);
-        return w;
-    }
-
-    @Override
-    public WebView createSubWebView(boolean privateBrowsing) {
-        return createWebView(privateBrowsing);
-    }
-
-    /**
-     * common webview initialization
-     * @param w the webview to initialize
-     */
-    protected void initWebViewSettings(WebView w) {
-        w.setScrollbarFadingEnabled(true);
-        w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
-        w.setMapTrackballToArrowKeys(false); // use trackball directly
-        // Enable the built-in zoom
-        w.getSettings().setBuiltInZoomControls(true);
-        boolean supportsMultiTouch = mActivity.getPackageManager()
-                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH);
-        w.getSettings().setDisplayZoomControls(!supportsMultiTouch);
-        w.setExpandedTileBounds(true);  // smoother scrolling
-
-        // Add this WebView to the settings observer list and update the
-        // settings
-        final BrowserSettings s = BrowserSettings.getInstance();
-        s.startManagingSettings(w.getSettings());
-    }
-
     private void cancelStopToast() {
         if (mStopToast != null) {
             mStopToast.cancel();
diff --git a/src/com/android/browser/Browser.java b/src/com/android/browser/Browser.java
index 65eb0ce..909a50d 100644
--- a/src/com/android/browser/Browser.java
+++ b/src/com/android/browser/Browser.java
@@ -58,6 +58,7 @@
         // create CookieSyncManager with current Context
         CookieSyncManager.createInstance(this);
         BrowserSettings.initialize(getApplicationContext());
+        Preloader.initialize(getApplicationContext());
     }
 
     static Intent createBrowserViewIntent() {
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index dbcae2e..13b8b06 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -92,7 +92,6 @@
             mUi = new PhoneUi(this, mController);
         }
         mController.setUi(mUi);
-        mController.setWebViewFactory((BaseUi) mUi);
 
         Bundle state = getIntent().getBundleExtra(EXTRA_STATE);
         if (state != null && icicle == null) {
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 4928e61..3a6349a 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -689,4 +689,11 @@
         return mPrefs.getBoolean(PREF_REMEMBER_PASSWORDS, true);
     }
 
+    // -----------------------------
+    // getter/setters for bandwidth_preferences.xml
+    // -----------------------------
+
+    public boolean isPreloadEnabled() {
+        return mPrefs.getBoolean(PREF_DATA_PRELOAD, false);
+    }
 }
diff --git a/src/com/android/browser/BrowserWebViewFactory.java b/src/com/android/browser/BrowserWebViewFactory.java
new file mode 100644
index 0000000..fbd26a9
--- /dev/null
+++ b/src/com/android/browser/BrowserWebViewFactory.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.AttributeSet;
+import android.view.View;
+import android.webkit.WebView;
+
+/**
+ * Web view factory class for creating {@link BrowserWebView}'s.
+ */
+public class BrowserWebViewFactory implements WebViewFactory {
+
+    private final Context mContext;
+
+    public BrowserWebViewFactory(Context context) {
+        mContext = context;
+    }
+
+    protected WebView instantiateWebView(AttributeSet attrs, int defStyle,
+            boolean privateBrowsing) {
+        return new BrowserWebView(mContext, attrs, defStyle, privateBrowsing);
+    }
+
+    @Override
+    public WebView createSubWebView(boolean privateBrowsing) {
+        return createWebView(privateBrowsing);
+    }
+
+    @Override
+    public WebView createWebView(boolean privateBrowsing) {
+        WebView w = instantiateWebView(null, android.R.attr.webViewStyle, privateBrowsing);
+        initWebViewSettings(w);
+        return w;
+    }
+
+    protected void initWebViewSettings(WebView w) {
+        w.setScrollbarFadingEnabled(true);
+        w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+        w.setMapTrackballToArrowKeys(false); // use trackball directly
+        // Enable the built-in zoom
+        w.getSettings().setBuiltInZoomControls(true);
+        boolean supportsMultiTouch = mContext.getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH);
+        w.getSettings().setDisplayZoomControls(!supportsMultiTouch);
+        w.setExpandedTileBounds(true);  // smoother scrolling
+
+        // Add this WebView to the settings observer list and update the
+        // settings
+        final BrowserSettings s = BrowserSettings.getInstance();
+        s.startManagingSettings(w.getSettings());
+    }
+
+}
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
index 447e61b..9046745 100644
--- a/src/com/android/browser/Controller.java
+++ b/src/com/android/browser/Controller.java
@@ -229,6 +229,7 @@
         mTabControl = new TabControl(this);
         mSettings.setController(this);
         mCrashRecoveryHandler = CrashRecoveryHandler.initialize(this);
+        mFactory = new BrowserWebViewFactory(browser);
 
         mUrlHandler = new UrlHandler(this);
         mIntentHandler = new IntentHandler(mActivity, this);
@@ -312,7 +313,7 @@
             // If the intent is ACTION_VIEW and data is not null, the Browser is
             // invoked to view the content by another application. In this case,
             // the tab will be close when exit.
-            UrlData urlData = mIntentHandler.getUrlDataFromIntent(intent);
+            UrlData urlData = IntentHandler.getUrlDataFromIntent(intent);
             Tab t = null;
             if (urlData.isEmpty()) {
                 t = openTabToHomePage();
@@ -356,10 +357,6 @@
         }
     }
 
-    void setWebViewFactory(WebViewFactory factory) {
-        mFactory = factory;
-    }
-
     @Override
     public WebViewFactory getWebViewFactory() {
         return mFactory;
@@ -381,6 +378,11 @@
     }
 
     @Override
+    public Context getContext() {
+        return mActivity;
+    }
+
+    @Override
     public Activity getActivity() {
         return mActivity;
     }
@@ -1637,6 +1639,7 @@
                         return id;
                     }
 
+                    @Override
                     protected void onPostExecute(Long id) {
                         if (id > 0) {
                             createNewSnapshotTab(id, true);
@@ -2247,11 +2250,19 @@
     // open a non inconito tab with the given url data
     // and set as active tab
     public Tab openTab(UrlData urlData) {
-        Tab tab = createNewTab(false, true, true);
-        if ((tab != null) && !urlData.isEmpty()) {
-            loadUrlDataIn(tab, urlData);
+        if (urlData.isPreloaded()) {
+            Tab tab = urlData.getPreloadedTab();
+            tab.getWebView().clearHistory();
+            mTabControl.addPreloadedTab(tab);
+            setActiveTab(tab);
+            return tab;
+        } else {
+            Tab tab = createNewTab(false, true, true);
+            if ((tab != null) && !urlData.isEmpty()) {
+                loadUrlDataIn(tab, urlData);
+            }
+            return tab;
         }
-        return tab;
     }
 
     @Override
@@ -2417,6 +2428,8 @@
         if (data != null) {
             if (data.mVoiceIntent != null) {
                 t.activateVoiceSearchMode(data.mVoiceIntent);
+            } else if (data.isPreloaded()) {
+                // this isn't called for preloaded tabs
             } else {
                 loadUrl(t, data.mUrl, data.mHeaders);
             }
diff --git a/src/com/android/browser/IntentHandler.java b/src/com/android/browser/IntentHandler.java
index 088a788..a99164a 100644
--- a/src/com/android/browser/IntentHandler.java
+++ b/src/com/android/browser/IntentHandler.java
@@ -135,8 +135,14 @@
                 urlData = new UrlData(mSettings.getHomePage());
             }
 
-            if (intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false)) {
-                mController.openTab(urlData);
+            if (intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false)
+                  || urlData.isPreloaded()) {
+                Tab t = mController.openTab(urlData);
+                if (t == null && urlData.isPreloaded()) {
+                    Tab pre = urlData.getPreloadedTab();
+                    // TODO: check if we need to stop loading
+                    pre.destroy();
+                }
                 return;
             }
             /*
@@ -220,9 +226,10 @@
         }
     }
 
-    protected UrlData getUrlDataFromIntent(Intent intent) {
+    protected static UrlData getUrlDataFromIntent(Intent intent) {
         String url = "";
         Map<String, String> headers = null;
+        Tab preloaded = null;
         if (intent != null
                 && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
             final String action = intent.getAction();
@@ -241,6 +248,10 @@
                         }
                     }
                 }
+                if (intent.hasExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID)) {
+                    String id = intent.getStringExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID);
+                    preloaded = Preloader.getInstance().getPreloadedTab(id);
+                }
             } else if (Intent.ACTION_SEARCH.equals(action)
                     || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
                     || Intent.ACTION_WEB_SEARCH.equals(action)) {
@@ -265,7 +276,7 @@
                 }
             }
         }
-        return new UrlData(url, headers, intent);
+        return new UrlData(url, headers, intent, preloaded);
     }
 
     /**
@@ -348,14 +359,20 @@
         final String mUrl;
         final Map<String, String> mHeaders;
         final Intent mVoiceIntent;
+        final Tab mPreloadedTab;
 
         UrlData(String url) {
             this.mUrl = url;
             this.mHeaders = null;
             this.mVoiceIntent = null;
+            this.mPreloadedTab = null;
         }
 
         UrlData(String url, Map<String, String> headers, Intent intent) {
+            this(url, headers, intent, null);
+        }
+
+        UrlData(String url, Map<String, String> headers, Intent intent, Tab preloaded) {
             this.mUrl = url;
             this.mHeaders = headers;
             if (RecognizerResultsIntent.ACTION_VOICE_SEARCH_RESULTS
@@ -364,11 +381,20 @@
             } else {
                 this.mVoiceIntent = null;
             }
+            mPreloadedTab = preloaded;
         }
 
         boolean isEmpty() {
             return mVoiceIntent == null && (mUrl == null || mUrl.length() == 0);
         }
+
+        boolean isPreloaded() {
+            return mPreloadedTab != null;
+        }
+
+        Tab getPreloadedTab() {
+            return mPreloadedTab;
+        }
     }
 
 }
diff --git a/src/com/android/browser/PreferenceKeys.java b/src/com/android/browser/PreferenceKeys.java
index bc8d38f..144d505 100644
--- a/src/com/android/browser/PreferenceKeys.java
+++ b/src/com/android/browser/PreferenceKeys.java
@@ -94,4 +94,9 @@
     static final String PREF_SAVE_FORMDATA = "save_formdata";
     static final String PREF_SHOW_SECURITY_WARNINGS = "show_security_warnings";
 
+    // ----------------------
+    // Keys for bandwidth_preferences.xml
+    // ----------------------
+    static final String PREF_DATA_PRELOAD = "preload_enabled";
+
 }
diff --git a/src/com/android/browser/PreloadController.java b/src/com/android/browser/PreloadController.java
new file mode 100644
index 0000000..6528410
--- /dev/null
+++ b/src/com/android/browser/PreloadController.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.view.View;
+import android.webkit.HttpAuthHandler;
+import android.webkit.SslErrorHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.webkit.WebView;
+
+import java.util.List;
+
+public class PreloadController implements WebViewController {
+
+    private Context mContext;
+
+    public PreloadController(Context ctx) {
+        mContext = ctx;
+
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public Activity getActivity() {
+        return null;
+    }
+
+    @Override
+    public TabControl getTabControl() {
+        return null;
+    }
+
+    @Override
+    public WebViewFactory getWebViewFactory() {
+        return null;
+    }
+
+    @Override
+    public void onSetWebView(Tab tab, WebView view) {
+    }
+
+    @Override
+    public void createSubWindow(Tab tab) {
+    }
+
+    @Override
+    public void onPageStarted(Tab tab, WebView view, Bitmap favicon) {
+    }
+
+    @Override
+    public void onPageFinished(Tab tab) {
+    }
+
+    @Override
+    public void onProgressChanged(Tab tab) {
+    }
+
+    @Override
+    public void onReceivedTitle(Tab tab, String title) {
+    }
+
+    @Override
+    public void onFavicon(Tab tab, WebView view, Bitmap icon) {
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+        return false;
+    }
+
+    @Override
+    public boolean shouldOverrideKeyEvent(KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void onUnhandledKeyEvent(KeyEvent event) {
+    }
+
+    @Override
+    public void doUpdateVisitedHistory(Tab tab, boolean isReload) {
+    }
+
+    @Override
+    public void getVisitedHistory(ValueCallback<String[]> callback) {
+    }
+
+    @Override
+    public void onReceivedHttpAuthRequest(Tab tab, WebView view,
+                                    HttpAuthHandler handler, String host,
+                                    String realm) {
+    }
+
+    @Override
+    public void onDownloadStart(Tab tab, String url, String useragent,
+                                    String contentDisposition, String mimeType,
+                                    long contentLength) {
+    }
+
+    @Override
+    public void showCustomView(Tab tab, View view, int requestedOrientation,
+                                    CustomViewCallback callback) {
+    }
+
+    @Override
+    public void hideCustomView() {
+    }
+
+    @Override
+    public Bitmap getDefaultVideoPoster() {
+        return null;
+    }
+
+    @Override
+    public View getVideoLoadingProgressView() {
+        return null;
+    }
+
+    @Override
+    public void showSslCertificateOnError(WebView view,
+                                    SslErrorHandler handler, SslError error) {
+    }
+
+    @Override
+    public void onUserCanceledSsl(Tab tab) {
+    }
+
+    @Override
+    public void activateVoiceSearchMode(String title, List<String> results) {
+    }
+
+    @Override
+    public void revertVoiceSearchMode(Tab tab) {
+    }
+
+    @Override
+    public boolean shouldShowErrorConsole() {
+        return false;
+    }
+
+    @Override
+    public void onUpdatedLockIcon(Tab tab) {
+    }
+
+    @Override
+    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
+    }
+
+    @Override
+    public void endActionMode() {
+    }
+
+    @Override
+    public void attachSubWindow(Tab tab) {
+    }
+
+    @Override
+    public void dismissSubWindow(Tab tab) {
+    }
+
+    @Override
+    public Tab openTab(String url, boolean incognito, boolean setActive,
+                                    boolean useCurrent) {
+        return null;
+    }
+
+    @Override
+    public Tab openTab(String url, Tab parent, boolean setActive,
+                                    boolean useCurrent) {
+        return null;
+    }
+
+    @Override
+    public boolean switchToTab(Tab tab) {
+        return false;
+    }
+
+    @Override
+    public void closeTab(Tab tab) {
+    }
+
+    @Override
+    public void setupAutoFill(Message message) {
+    }
+
+    @Override
+    public void bookmarkedStatusHasChanged(Tab tab) {
+    }
+
+    @Override
+    public void showAutoLogin(Tab tab) {
+    }
+
+    @Override
+    public void hideAutoLogin(Tab tab) {
+    }
+
+}
diff --git a/src/com/android/browser/PreloadRequestReceiver.java b/src/com/android/browser/PreloadRequestReceiver.java
new file mode 100644
index 0000000..c86d660
--- /dev/null
+++ b/src/com/android/browser/PreloadRequestReceiver.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Broadcast receiver for receiving browser preload requests
+ */
+public class PreloadRequestReceiver extends BroadcastReceiver {
+
+    private final static String LOGTAG = "browser.preloader";
+    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+
+    private static final String ACTION_PRELOAD = "android.intent.action.PRELOAD";
+    static final String EXTRA_PRELOAD_ID = "preload_id";
+    static final String EXTRA_PRELOAD_DISCARD = "preload_discard";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "received intent " + intent);
+        if (BrowserSettings.getInstance().isPreloadEnabled()
+                && intent.getAction().equals(ACTION_PRELOAD)) {
+            handlePreload(context, intent);
+        }
+    }
+
+    private void handlePreload(Context context, Intent i) {
+        String url = UrlUtils.smartUrlFilter(i.getData());
+        String id = i.getStringExtra(EXTRA_PRELOAD_ID);
+        Map<String, String> headers = null;
+        if (id == null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Preload request has no " + EXTRA_PRELOAD_ID);
+            return;
+        }
+        if (i.getBooleanExtra(EXTRA_PRELOAD_DISCARD, false)) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload discard request");
+            Preloader.getInstance().discardPreload(id);
+        } else {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload request for " + url);
+            if (url != null && url.startsWith("http")) {
+                final Bundle pairs = i.getBundleExtra(Browser.EXTRA_HEADERS);
+                if (pairs != null && !pairs.isEmpty()) {
+                    Iterator<String> iter = pairs.keySet().iterator();
+                    headers = new HashMap<String, String>();
+                    while (iter.hasNext()) {
+                        String key = iter.next();
+                        headers.put(key, pairs.getString(key));
+                    }
+                }
+            }
+            if (url != null) {
+                Preloader.getInstance().handlePreloadRequest(id, url, headers);
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/browser/Preloader.java b/src/com/android/browser/Preloader.java
new file mode 100644
index 0000000..5a5f687
--- /dev/null
+++ b/src/com/android/browser/Preloader.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.webkit.WebView;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Singleton class for handling preload requests.
+ */
+public class Preloader {
+
+    private final static String LOGTAG = "browser.preloader";
+    private final static boolean LOGD_ENABLED = true;//com.android.browser.Browser.LOGD_ENABLED;
+
+    private static final int PRERENDER_TIMEOUT_MILLIS = 30 * 1000; // 30s
+
+    private static Preloader sInstance;
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final BrowserWebViewFactory mFactory;
+    private final HashMap<String, PreloaderSession> mSessions;
+
+    public static void initialize(Context context) {
+        sInstance = new Preloader(context);
+    }
+
+    public static Preloader getInstance() {
+        return sInstance;
+    }
+
+    private Preloader(Context context) {
+        mContext = context;
+        mHandler = new Handler(Looper.getMainLooper());
+        mSessions = new HashMap<String, PreloaderSession>();
+        mFactory = new BrowserWebViewFactory(context);
+
+    }
+
+    private PreloaderSession getSession(String id) {
+        PreloaderSession s = mSessions.get(id);
+        if (s == null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Create new preload session " + id);
+            s = new PreloaderSession(id);
+            mSessions.put(id, s);
+        }
+        return s;
+    }
+
+    private PreloaderSession takeSession(String id) {
+        PreloaderSession s = mSessions.remove(id);
+        if (s != null) {
+            s.cancelTimeout();
+        }
+        return s;
+    }
+
+    public void handlePreloadRequest(String id, String url, Map<String, String> headers) {
+        PreloaderSession s = getSession(id);
+        s.touch(); // reset timer
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Preloading " + url);
+        s.getTab().loadUrl(url, headers);
+    }
+
+    public void discardPreload(String id) {
+        PreloaderSession s = takeSession(id);
+        if (s != null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Discard preload session " + id);
+            Tab t = s.getTab();
+            t.destroy();
+        }
+    }
+
+    /**
+     * Return a preloaded tab, and remove it from the preloader. This is used when the
+     * view is about to be displayed.
+     */
+    public Tab getPreloadedTab(String id) {
+        PreloaderSession s = takeSession(id);
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Showing preload session " + id + "=" + s);
+        return s == null ? null : s.getTab();
+    }
+
+    private class PreloaderSession {
+        private final String mId;
+        private final Tab mTab;
+
+        private final Runnable mTimeoutTask = new Runnable(){
+            @Override
+            public void run() {
+                if (LOGD_ENABLED) Log.d(LOGTAG, "Preload session timeout " + mId);
+                discardPreload(mId);
+            }};
+
+        public PreloaderSession(String id) {
+            mId = id;
+            mTab = new Tab(new PreloadController(mContext), mFactory.createWebView(false));
+            touch();
+        }
+
+        public void cancelTimeout() {
+            mHandler.removeCallbacks(mTimeoutTask);
+        }
+
+        public void touch() {
+            cancelTimeout();
+            mHandler.postDelayed(mTimeoutTask, PRERENDER_TIMEOUT_MILLIS);
+        }
+
+        public Tab getTab() {
+            return mTab;
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/SnapshotTab.java b/src/com/android/browser/SnapshotTab.java
index 52a5c5f..adccdf3 100644
--- a/src/com/android/browser/SnapshotTab.java
+++ b/src/com/android/browser/SnapshotTab.java
@@ -76,7 +76,7 @@
 
     void loadData() {
         if (mLoadTask == null) {
-            mLoadTask = new LoadData(this, mActivity.getContentResolver());
+            mLoadTask = new LoadData(this, mContext.getContentResolver());
             mLoadTask.execute();
         }
     }
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index e672e2b..f8687a8 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -92,7 +92,7 @@
         LOCK_ICON_MIXED,
     }
 
-    Activity mActivity;
+    Context mContext;
     protected WebViewController mWebViewController;
 
     // The tab ID
@@ -304,7 +304,7 @@
                 logIntent.putExtra(
                         LoggingEvents.VoiceSearch.EXTRA_N_BEST_CHOOSE_INDEX,
                         index);
-                mActivity.sendBroadcast(logIntent);
+                mContext.sendBroadcast(logIntent);
             }
             if (mVoiceSearchData.mVoiceSearchIntent != null) {
                 // Copy the Intent, so that each history item will have its own
@@ -487,7 +487,7 @@
 
     private void showError(ErrorDialog errDialog) {
         if (mInForeground) {
-            AlertDialog d = new AlertDialog.Builder(mActivity)
+            AlertDialog d = new AlertDialog.Builder(mContext)
                     .setTitle(errDialog.mTitle)
                     .setMessage(errDialog.mDescription)
                     .setPositiveButton(R.string.ok, null)
@@ -508,7 +508,7 @@
         public void onPageStarted(WebView view, String url, Bitmap favicon) {
             mInPageLoad = true;
             mPageLoadProgress = 0;
-            mCurrentState = new PageState(mActivity,
+            mCurrentState = new PageState(mContext,
                     view.isPrivateBrowsingEnabled(), url, favicon);
             mLoadStartTime = SystemClock.uptimeMillis();
             if (mVoiceSearchData != null
@@ -516,7 +516,7 @@
                 if (mVoiceSearchData.mSourceIsGoogle) {
                     Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT);
                     i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
-                    mActivity.sendBroadcast(i);
+                    mContext.sendBroadcast(i);
                 }
                 revertVoiceSearchMode();
             }
@@ -587,7 +587,7 @@
                 Intent logIntent = new Intent(LoggingEvents.ACTION_LOG_EVENT);
                 logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
                         LoggingEvents.VoiceSearch.RESULT_CLICKED);
-                mActivity.sendBroadcast(logIntent);
+                mContext.sendBroadcast(logIntent);
             }
             if (mInForeground) {
                 return mWebViewController.shouldOverrideUrlLoading(Tab.this,
@@ -659,7 +659,7 @@
             }
             mDontResend = dontResend;
             mResend = resend;
-            new AlertDialog.Builder(mActivity).setTitle(
+            new AlertDialog.Builder(mContext).setTitle(
                     R.string.browserFrameFormResubmitLabel).setMessage(
                     R.string.browserFrameFormResubmitMessage)
                     .setPositiveButton(R.string.ok,
@@ -718,7 +718,7 @@
             }
             if (mSettings.showSecurityWarnings()) {
                 final LayoutInflater factory =
-                    LayoutInflater.from(mActivity);
+                    LayoutInflater.from(mContext);
                 final View warningsView =
                     factory.inflate(R.layout.ssl_warnings, null);
                 final LinearLayout placeholder =
@@ -756,7 +756,7 @@
                     placeholder.addView(ll);
                 }
 
-                new AlertDialog.Builder(mActivity).setTitle(
+                new AlertDialog.Builder(mContext).setTitle(
                         R.string.security_warning).setIcon(
                         android.R.drawable.ic_dialog_alert).setView(
                         warningsView).setPositiveButton(R.string.ssl_continue,
@@ -817,13 +817,14 @@
                     port = -1;
                 }
             }
-            KeyChain.choosePrivateKeyAlias(mActivity, new KeyChainAliasCallback() {
+            KeyChain.choosePrivateKeyAlias(
+                    mWebViewController.getActivity(), new KeyChainAliasCallback() {
                 @Override public void alias(String alias) {
                     if (alias == null) {
                         handler.cancel();
                         return;
                     }
-                    new KeyChainLookup(mActivity, handler, alias).execute();
+                    new KeyChainLookup(mContext, handler, alias).execute();
                 }
             }, null, null, host, port, null);
         }
@@ -846,7 +847,7 @@
         public WebResourceResponse shouldInterceptRequest(WebView view,
                 String url) {
             WebResourceResponse res = HomeProvider.shouldInterceptRequest(
-                    mActivity, url);
+                    mContext, url);
             return res;
         }
 
@@ -869,7 +870,7 @@
         @Override
         public void onReceivedLoginRequest(WebView view, String realm,
                 String account, String args) {
-            new DeviceAccountLogin(mActivity, view, Tab.this, mWebViewController)
+            new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController)
                     .handleLogin(realm, account, args);
         }
 
@@ -916,7 +917,7 @@
             }
             // Short-circuit if we can't create any more tabs or sub windows.
             if (dialog && mSubView != null) {
-                new AlertDialog.Builder(mActivity)
+                new AlertDialog.Builder(mContext)
                         .setTitle(R.string.too_many_subwindows_dialog_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.too_many_subwindows_dialog_message)
@@ -924,7 +925,7 @@
                         .show();
                 return false;
             } else if (!mWebViewController.getTabControl().canCreateNewTab()) {
-                new AlertDialog.Builder(mActivity)
+                new AlertDialog.Builder(mContext)
                         .setTitle(R.string.too_many_windows_dialog_title)
                         .setIcon(android.R.drawable.ic_dialog_alert)
                         .setMessage(R.string.too_many_windows_dialog_message)
@@ -958,7 +959,7 @@
 
             // Build a confirmation dialog to display to the user.
             final AlertDialog d =
-                    new AlertDialog.Builder(mActivity)
+                    new AlertDialog.Builder(mContext)
                     .setTitle(R.string.attention)
                     .setIcon(android.R.drawable.ic_dialog_alert)
                     .setMessage(R.string.popup_window_attempt)
@@ -1011,7 +1012,7 @@
         @Override
         public void onReceivedTouchIconUrl(WebView view, String url,
                 boolean precomposed) {
-            final ContentResolver cr = mActivity.getContentResolver();
+            final ContentResolver cr = mContext.getContentResolver();
             // Let precomposed icons take precedence over non-composed
             // icons.
             if (precomposed && mTouchIconLoader != null) {
@@ -1021,7 +1022,7 @@
             // Have only one async task at a time.
             if (mTouchIconLoader == null) {
                 mTouchIconLoader = new DownloadTouchIcon(Tab.this,
-                        mActivity, cr, view);
+                        mContext, cr, view);
                 mTouchIconLoader.execute(url);
             }
         }
@@ -1029,7 +1030,10 @@
         @Override
         public void onShowCustomView(View view,
                 WebChromeClient.CustomViewCallback callback) {
-            onShowCustomView(view, mActivity.getRequestedOrientation(), callback);
+            Activity activity = mWebViewController.getActivity();
+            if (activity != null) {
+                onShowCustomView(view, activity.getRequestedOrientation(), callback);
+            }
         }
 
         @Override
@@ -1202,8 +1206,8 @@
         public void setupAutoFill(Message message) {
             // Prompt the user to set up their profile.
             final Message msg = message;
-            AlertDialog.Builder builder = new AlertDialog.Builder(mActivity);
-            LayoutInflater inflater = (LayoutInflater) mActivity.getSystemService(
+            AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
                     Context.LAYOUT_INFLATER_SERVICE);
             final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null);
 
@@ -1217,7 +1221,7 @@
                         if (disableAutoFill.isChecked()) {
                             // Disable autofill and show a toast with how to turn it on again.
                             mSettings.setAutofillEnabled(false);
-                            Toast.makeText(mActivity,
+                            Toast.makeText(mContext,
                                     R.string.autofill_setup_dialog_negative_toast,
                                     Toast.LENGTH_LONG).show();
                         } else {
@@ -1338,10 +1342,10 @@
     // Construct a new tab
     Tab(WebViewController wvcontroller, WebView w) {
         mWebViewController = wvcontroller;
-        mActivity = mWebViewController.getActivity();
+        mContext = mWebViewController.getContext();
         mSettings = BrowserSettings.getInstance();
-        mDataController = DataController.getInstance(mActivity);
-        mCurrentState = new PageState(mActivity, w != null
+        mDataController = DataController.getInstance(mContext);
+        mCurrentState = new PageState(mContext, w != null
                 ? w.isPrivateBrowsingEnabled() : false);
         mInPageLoad = false;
         mInForeground = false;
@@ -1373,6 +1377,10 @@
         setWebView(w);
     }
 
+    public void setController(WebViewController ctl) {
+        mWebViewController = ctl;
+    }
+
     public void setId(long id) {
         mId = id;
     }
@@ -1468,7 +1476,7 @@
                     }
                 }
             });
-            mSubView.setOnCreateContextMenuListener(mActivity);
+            mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity());
             return true;
         }
         return false;
@@ -1558,9 +1566,10 @@
     void putInForeground() {
         mInForeground = true;
         resume();
-        mMainView.setOnCreateContextMenuListener(mActivity);
+        Activity activity = mWebViewController.getActivity();
+        mMainView.setOnCreateContextMenuListener(activity);
         if (mSubView != null) {
-            mSubView.setOnCreateContextMenuListener(mActivity);
+            mSubView.setOnCreateContextMenuListener(activity);
         }
         // Show the pending error dialog if the queue is not empty
         if (mQueuedErrors != null && mQueuedErrors.size() >  0) {
@@ -1690,7 +1699,7 @@
      */
     String getTitle() {
         if (mCurrentState.mTitle == null && mInPageLoad) {
-            return mActivity.getString(R.string.title_bar_loading);
+            return mContext.getString(R.string.title_bar_loading);
         }
         return mCurrentState.mTitle;
     }
@@ -1716,7 +1725,7 @@
      */
     ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
         if (createIfNecessary && mErrorConsole == null) {
-            mErrorConsole = new ErrorConsoleView(mActivity);
+            mErrorConsole = new ErrorConsoleView(mContext);
             mErrorConsole.setWebView(mMainView);
         }
         return mErrorConsole;
@@ -1882,7 +1891,7 @@
 
     public void loadUrl(String url, Map<String, String> headers) {
         if (mMainView != null) {
-            mCurrentState = new PageState(mActivity, false, url, null);
+            mCurrentState = new PageState(mContext, false, url, null);
             mWebViewController.onPageStarted(this, mMainView, null);
             mMainView.loadUrl(url, headers);
         }
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
index 2eb24e9..1110bda 100644
--- a/src/com/android/browser/TabControl.java
+++ b/src/com/android/browser/TabControl.java
@@ -176,6 +176,14 @@
         return false;
     }
 
+    void addPreloadedTab(Tab tab) {
+        tab.setId(getNextId());
+        mTabs.add(tab);
+        tab.setController(mController);
+        mController.onSetWebView(tab, tab.getWebView());
+        tab.putInBackground();
+    }
+
     /**
      * Create a new tab.
      * @return The newly createTab or null if we have reached the maximum
diff --git a/src/com/android/browser/WebViewController.java b/src/com/android/browser/WebViewController.java
index 018af99..175cbf8 100644
--- a/src/com/android/browser/WebViewController.java
+++ b/src/com/android/browser/WebViewController.java
@@ -17,6 +17,7 @@
 package com.android.browser;
 
 import android.app.Activity;
+import android.content.Context;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.net.http.SslError;
@@ -36,6 +37,8 @@
  */
 public interface WebViewController {
 
+    Context getContext();
+
     Activity getActivity();
 
     TabControl getTabControl();
@@ -72,7 +75,7 @@
     void onDownloadStart(Tab tab, String url, String useragent, String contentDisposition,
             String mimeType, long contentLength);
 
-    void showCustomView(Tab tab, View view, int requestedOrientation, 
+    void showCustomView(Tab tab, View view, int requestedOrientation,
             WebChromeClient.CustomViewCallback callback);
 
     void hideCustomView();
diff --git a/src/com/android/browser/XLargeUi.java b/src/com/android/browser/XLargeUi.java
index 8b43a74..8455e74 100644
--- a/src/com/android/browser/XLargeUi.java
+++ b/src/com/android/browser/XLargeUi.java
@@ -70,6 +70,14 @@
     }
 
     @Override
+    public void onSetWebView(Tab tab, WebView v) {
+        super.onSetWebView(tab, v);
+        if (v != null) {
+            ((BrowserWebView) v).setScrollListener(this);
+        }
+    }
+
+    @Override
     public void showComboView(boolean startWithHistory, Bundle extras) {
         super.showComboView(startWithHistory, extras);
         if (mUseQuickControls) {
@@ -135,21 +143,6 @@
         hideTitleBar();
     }
 
-    // webview factory
-
-    @Override
-    public WebView createWebView(boolean privateBrowsing) {
-        // Create a new WebView
-        BrowserWebView w = (BrowserWebView) super.createWebView(privateBrowsing);
-        w.setScrollListener(this);
-        return w;
-    }
-
-    @Override
-    public WebView createSubWebView(boolean privateBrowsing) {
-        return super.createWebView(privateBrowsing);
-    }
-
     @Override
     public void onScroll(int visibleTitleHeight, boolean userInitiated) {
         mTabBar.onScroll(visibleTitleHeight, userInitiated);
diff --git a/src/com/android/browser/preferences/BandwidthPreferencesFragment.java b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
new file mode 100644
index 0000000..18b9fa4
--- /dev/null
+++ b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.browser.preferences;
+
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.util.Log;
+
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class BandwidthPreferencesFragment extends PreferenceFragment {
+
+    static final String TAG = "BandwidthPreferencesFragment";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.bandwidth_preferences);
+    }
+
+}