Merge "Remove unused strings."
diff --git a/Android.mk b/Android.mk
index 537ad60..6e20ab8 100644
--- a/Android.mk
+++ b/Android.mk
@@ -4,11 +4,10 @@
 LOCAL_MODULE_TAGS := optional
 
 LOCAL_STATIC_JAVA_LIBRARIES := \
-        android-common \
-        gsf-client
+        android-common
 
 LOCAL_SRC_FILES := \
-        $(call all-subdir-java-files) \
+        $(call all-java-files-under, src) \
         src/com/android/browser/EventLogTags.logtags
 
 LOCAL_PACKAGE_NAME := Browser
diff --git a/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark.png b/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark.png
index f861650..7b2c680 100644
--- a/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark.png
+++ b/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark_icon.png b/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark_icon.png
new file mode 100644
index 0000000..57fc915
--- /dev/null
+++ b/res/drawable-hdpi/ic_launcher_shortcut_browser_bookmark_icon.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_launcher_shortcut_browser_bookmark_icon.png b/res/drawable-mdpi/ic_launcher_shortcut_browser_bookmark_icon.png
new file mode 100644
index 0000000..ba82911
--- /dev/null
+++ b/res/drawable-mdpi/ic_launcher_shortcut_browser_bookmark_icon.png
Binary files differ
diff --git a/res/layout/title_bar.xml b/res/layout/title_bar.xml
index d619d6b..f5c6d6d 100644
--- a/res/layout/title_bar.xml
+++ b/res/layout/title_bar.xml
@@ -76,9 +76,11 @@
             android:id="@+id/rt_btn"
             android:layout_width="wrap_content"
             android:layout_height="match_parent"
-            android:layout_marginLeft="6dip"
+            android:layout_marginLeft="-2dip"
+            android:layout_marginTop="-6.5dip"
+            android:layout_marginBottom="-2dip"
+            android:layout_marginRight="-5dip"
             android:scaleType="center"
-            android:layout_marginBottom="4dip"
             android:background="@drawable/btn_bookmark"
             android:src="@drawable/ic_btn_bookmarks"
         />
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index fc337f9..f860b1f 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -73,6 +73,7 @@
 import android.text.format.DateFormat;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.util.Patterns;
 import android.view.ContextMenu;
 import android.view.Gravity;
 import android.view.KeyEvent;
@@ -110,12 +111,9 @@
 import android.accounts.OperationCanceledException;
 import android.accounts.AccountManagerCallback;
 
-import com.android.common.Patterns;
 import com.android.common.Search;
 import com.android.common.speech.LoggingEvents;
 
-import com.google.android.gsf.GoogleLoginServiceConstants;
-
 import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.IOException;
@@ -157,13 +155,20 @@
     private Account[] mAccountsGoogle;
     private Account[] mAccountsPreferHosted;
 
+    // XXX: These constants should be exposed through some public api. Hardcode
+    // the values for now until some solution for gsf can be worked out.
+    // http://b/issue?id=2425179
+    private static final String ACCOUNT_TYPE = "com.google";
+    private static final String FEATURE_LEGACY_GOOGLE = "legacy_google";
+    private static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE =
+            "legacy_hosted_or_google";
+
     private void startReadOfGoogleAccounts() {
         mAccountsGoogle = null;
         mAccountsPreferHosted = null;
 
         AccountManager.get(this).getAccountsByTypeAndFeatures(
-                GoogleLoginServiceConstants.ACCOUNT_TYPE,
-                new String[]{GoogleLoginServiceConstants.FEATURE_LEGACY_HOSTED_OR_GOOGLE},
+                ACCOUNT_TYPE, new String[]{ FEATURE_LEGACY_HOSTED_OR_GOOGLE },
                 this, null);
     }
 
@@ -174,8 +179,7 @@
                 mAccountsGoogle = accountManagerFuture.getResult();
 
                 AccountManager.get(this).getAccountsByTypeAndFeatures(
-                        GoogleLoginServiceConstants.ACCOUNT_TYPE,
-                        new String[]{GoogleLoginServiceConstants.FEATURE_LEGACY_GOOGLE},
+                        ACCOUNT_TYPE, new String[]{ FEATURE_LEGACY_GOOGLE },
                         this, null);
             } else {
                 mAccountsPreferHosted = accountManagerFuture.getResult();
@@ -2149,7 +2153,7 @@
         resetTitleIconAndProgress();
     }
 
-    private void goBackOnePageOrQuit() {
+    /* package */ void goBackOnePageOrQuit() {
         Tab current = mTabControl.getCurrentTab();
         if (current == null) {
             /*
@@ -2461,7 +2465,7 @@
      * an {@link Intent} to launch the Activity chooser.
      * @param c Context used to launch a new Activity.
      * @param title Title of the page.  Stored in the Intent with
-     *          {@link Browser#EXTRA_SHARE_TITLE}
+     *          {@link Intent#EXTRA_SUBJECT}
      * @param url URL of the page.  Stored in the Intent with
      *          {@link Intent#EXTRA_TEXT}
      * @param favicon Bitmap of the favicon for the page.  Stored in the Intent
@@ -2474,7 +2478,7 @@
         Intent send = new Intent(Intent.ACTION_SEND);
         send.setType("text/plain");
         send.putExtra(Intent.EXTRA_TEXT, url);
-        send.putExtra(Browser.EXTRA_SHARE_TITLE, title);
+        send.putExtra(Intent.EXTRA_SUBJECT, title);
         send.putExtra(Browser.EXTRA_SHARE_FAVICON, favicon);
         send.putExtra(Browser.EXTRA_SHARE_SCREENSHOT, screenshot);
         try {
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
index d835f84..1183b70 100644
--- a/src/com/android/browser/BrowserBookmarksPage.java
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -30,6 +30,7 @@
 import android.graphics.Path;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
 import android.graphics.RectF;
 import android.net.Uri;
 import android.os.Bundle;
@@ -445,7 +446,7 @@
                                 R.drawable.ic_launcher_shortcut_browser_bookmark));
             } else {
                 Bitmap icon = BitmapFactory.decodeResource(getResources(),
-                        R.drawable.ic_launcher_shortcut_browser_bookmark);
+                        R.drawable.ic_launcher_shortcut_browser_bookmark_icon);
 
                 // Make a copy of the regular icon so we can modify the pixels.
                 Bitmap copy = icon.copy(Bitmap.Config.ARGB_8888, true);
@@ -458,13 +459,22 @@
                 p.setStyle(Paint.Style.FILL_AND_STROKE);
                 p.setColor(Color.WHITE);
 
-                float density = getResources().getDisplayMetrics().density;
+                final float density =
+                        getResources().getDisplayMetrics().density;
                 // Create a rectangle that is slightly wider than the favicon
                 final float iconSize = 16 * density; // 16x16 favicon
-                final float padding = 2; // white padding around icon
+                final float padding = 2 * density; // white padding around icon
                 final float rectSize = iconSize + 2 * padding;
-                final float y = icon.getHeight() - rectSize;
-                RectF r = new RectF(0, y, rectSize, y + rectSize);
+
+                final Rect iconBounds =
+                        new Rect(0, 0, icon.getWidth(), icon.getHeight());
+                final float x = iconBounds.exactCenterX() - (rectSize / 2);
+                // Note: Subtract 2 dip from the y position since the box is
+                // slightly higher than center. Use padding since it is already
+                // 2 * density.
+                final float y = iconBounds.exactCenterY() - (rectSize / 2)
+                        - padding;
+                RectF r = new RectF(x, y, x + rectSize, y + rectSize);
 
                 // Draw a white rounded rectangle behind the favicon
                 canvas.drawRoundRect(r, 2, 2, p);
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index dda5765..94d7eca 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -46,12 +46,10 @@
 import android.speech.RecognizerResultsIntent;
 import android.text.TextUtils;
 import android.util.Log;
+import android.util.Patterns;
 import android.util.TypedValue;
 import android.webkit.GeolocationPermissions;
 
-import com.android.common.Patterns;
-
-import com.google.android.gsf.GoogleSettingsContract.Partner;
 
 import java.io.File;
 import java.io.FilenameFilter;
@@ -181,13 +179,32 @@
     public BrowserProvider() {
     }
 
+    // XXX: This is a major hack to remove our dependency on gsf constants and
+    // its content provider. http://b/issue?id=2425179
+    static String getClientId(ContentResolver cr) {
+        String ret = "android-google";
+        Cursor c = null;
+        try {
+            c = cr.query(Uri.parse("content://com.google.settings/partner"),
+                    new String[] { "value" }, "name='client_id'", null, null);
+            if (c != null && c.moveToNext()) {
+                ret = c.getString(0);
+            }
+        } catch (RuntimeException ex) {
+            // fall through to return the default
+        } finally {
+            if (c != null) {
+                c.close();
+            }
+        }
+        return ret;
+    }
 
     private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
         StringBuffer sb = new StringBuffer();
         int lastCharLoc = 0;
 
-        final String client_id = Partner.getString(context.getContentResolver(),
-                                                    Partner.CLIENT_ID, "android-google");
+        final String client_id = getClientId(context.getContentResolver());
 
         for (int i = 0; i < srcString.length(); ++i) {
             char c = srcString.charAt(i);
@@ -1056,18 +1073,14 @@
             throw new IllegalArgumentException("Unknown URL");
         }
 
-        String id = null;
-        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
-        boolean changingBookmarks = false;
-
-        if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
+        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
             StringBuilder sb = new StringBuilder();
             if (where != null && where.length() > 0) {
                 sb.append("( ");
                 sb.append(where);
                 sb.append(" ) AND ");
             }
-            id = url.getPathSegments().get(1);
+            String id = url.getPathSegments().get(1);
             sb.append("_id = ");
             sb.append(id);
             where = sb.toString();
@@ -1078,23 +1091,23 @@
         // Not all bookmark-table updates should be backed up.  Look to see
         // whether we changed the title, url, or "is a bookmark" state, and
         // request a backup if so.
-        if (isBookmarkTable) {
+        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) {
+            boolean changingBookmarks = false;
             // Alterations to the bookmark field inherently change the bookmark
             // set, so we don't need to query the record; we know a priori that
             // we will need to back up this change.
             if (values.containsKey(BookmarkColumns.BOOKMARK)) {
                 changingBookmarks = true;
-            }
-            // changing the title or URL of a bookmark record requires a backup,
-            // but we don't know wether such an update is on a bookmark without
-            // querying the record
-            if (!changingBookmarks &&
-                    (values.containsKey(BookmarkColumns.TITLE)
-                     || values.containsKey(BookmarkColumns.URL))) {
-                // when isBookmarkTable is true, the 'id' var was assigned above
+            } else if ((values.containsKey(BookmarkColumns.TITLE)
+                     || values.containsKey(BookmarkColumns.URL))
+                     && values.containsKey(BookmarkColumns._ID)) {
+                // If a title or URL has been changed, check to see if it is to
+                // a bookmark.  The ID should have been included in the update,
+                // so use it.
                 Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
                         new String[] { BookmarkColumns.BOOKMARK },
-                        "_id = " + id, null, null);
+                        BookmarkColumns._ID + " = "
+                        + values.getAsString(BookmarkColumns._ID), null, null);
                 if (cursor.moveToNext()) {
                     changingBookmarks = (cursor.getInt(0) != 0);
                 }
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 1e282c6..fbff0fa 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -17,8 +17,6 @@
 
 package com.android.browser;
 
-import com.google.android.gsf.GoogleSettingsContract.Partner;
-
 import android.app.ActivityManager;
 import android.content.ContentResolver;
 import android.content.Context;
@@ -578,8 +576,8 @@
     private String getFactoryResetHomeUrl(Context context) {
         String url = context.getResources().getString(R.string.homepage_base);
         if (url.indexOf("{CID}") != -1) {
-            url = url.replace("{CID}", Partner.getString(context
-                    .getContentResolver(), Partner.CLIENT_ID, "android-google"));
+            url = url.replace("{CID}",
+                    BrowserProvider.getClientId(context.getContentResolver()));
         }
         return url;
     }
diff --git a/src/com/android/browser/DownloadTouchIcon.java b/src/com/android/browser/DownloadTouchIcon.java
index 22ed73c..e2d4594 100644
--- a/src/com/android/browser/DownloadTouchIcon.java
+++ b/src/com/android/browser/DownloadTouchIcon.java
@@ -22,11 +22,11 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.net.http.AndroidHttpClient;
 import android.os.AsyncTask;
 import android.provider.Browser;
 import android.webkit.WebView;
 
-import com.android.common.AndroidHttpClient;
 
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
index 9e34736..1e4debf 100644
--- a/src/com/android/browser/FetchUrlMimeType.java
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -18,7 +18,7 @@
 
 import android.content.ContentValues;
 import android.net.Uri;
-import com.android.common.AndroidHttpClient;
+import android.net.http.AndroidHttpClient;
 
 import org.apache.http.HttpResponse;
 import org.apache.http.Header;
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index afd9b09..512f2b7 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -48,6 +48,7 @@
 import android.view.View.OnClickListener;
 import android.webkit.ConsoleMessage;
 import android.webkit.CookieSyncManager;
+import android.webkit.DownloadListener;
 import android.webkit.GeolocationPermissions;
 import android.webkit.HttpAuthHandler;
 import android.webkit.SslErrorHandler;
@@ -123,6 +124,9 @@
     private final LayoutInflater mInflateService;
     // The BrowserActivity which owners the Tab
     private final BrowserActivity mActivity;
+    // The listener that gets invoked when a download is started from the
+    // mMainView
+    private final DownloadListener mDownloadListener;
 
     // AsyncTask for downloading touch icons
     DownloadTouchIcon mTouchIconLoader;
@@ -1224,6 +1228,27 @@
             (GeolocationPermissionsPrompt) mContainer.findViewById(
                 R.id.geolocation_permissions_prompt);
 
+        mDownloadListener = new DownloadListener() {
+            public void onDownloadStart(String url, String userAgent,
+                    String contentDisposition, String mimetype,
+                    long contentLength) {
+                mActivity.onDownloadStart(url, userAgent, contentDisposition,
+                        mimetype, contentLength);
+                if (mMainView.copyBackForwardList().getSize() == 0) {
+                    // This Tab was opened for the sole purpose of downloading a
+                    // file. Remove it.
+                    if (mActivity.getTabControl().getCurrentWebView()
+                            == mMainView) {
+                        // In this case, the Tab is still on top.
+                        mActivity.goBackOnePageOrQuit();
+                    } else {
+                        // In this case, it is not.
+                        mActivity.closeTab(Tab.this);
+                    }
+                }
+            }
+        };
+
         setWebView(w);
     }
 
@@ -1246,10 +1271,15 @@
 
         // set the new one
         mMainView = w;
-        // attached the WebViewClient and WebChromeClient
+        // attach the WebViewClient, WebChromeClient and DownloadListener
         if (mMainView != null) {
             mMainView.setWebViewClient(mWebViewClient);
             mMainView.setWebChromeClient(mWebChromeClient);
+            // Attach DownloadManager so that downloads can start in an active
+            // or a non-active window. This can happen when going to a site that
+            // does a redirect after a period of time. The user could have
+            // switched to another tab while waiting for the download to start.
+            mMainView.setDownloadListener(mDownloadListener);
         }
     }
 
@@ -1297,7 +1327,21 @@
             mSubView.setWebViewClient(new SubWindowClient(mWebViewClient));
             mSubView.setWebChromeClient(new SubWindowChromeClient(
                     mWebChromeClient));
-            mSubView.setDownloadListener(mActivity);
+            // Set a different DownloadListener for the mSubView, since it will
+            // just need to dismiss the mSubView, rather than close the Tab
+            mSubView.setDownloadListener(new DownloadListener() {
+                public void onDownloadStart(String url, String userAgent,
+                        String contentDisposition, String mimetype,
+                        long contentLength) {
+                    mActivity.onDownloadStart(url, userAgent,
+                            contentDisposition, mimetype, contentLength);
+                    if (mSubView.copyBackForwardList().getSize() == 0) {
+                        // This subwindow was opened for the sole purpose of
+                        // downloading a file. Remove it.
+                        dismissSubWindow();
+                    }
+                }
+            });
             mSubView.setOnCreateContextMenuListener(mActivity);
             final BrowserSettings s = BrowserSettings.getInstance();
             s.addObserver(mSubView.getSettings()).update(s, null);
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
index 1790098..e64f3fb 100644
--- a/src/com/android/browser/TabControl.java
+++ b/src/com/android/browser/TabControl.java
@@ -533,18 +533,13 @@
         w.setMapTrackballToArrowKeys(false); // use trackball directly
         // Enable the built-in zoom
         w.getSettings().setBuiltInZoomControls(true);
-        // Attach DownloadManager so that downloads can start in an active or
-        // a non-active window. This can happen when going to a site that does
-        // a redirect after a period of time. The user could have switched to
-        // another tab while waiting for the download to start.
-        w.setDownloadListener(mActivity);
         // Add this WebView to the settings observer list and update the
         // settings
         final BrowserSettings s = BrowserSettings.getInstance();
         s.addObserver(w.getSettings()).update(s, null);
 
         // pick a default
-        if (true) {
+        if (false) {
             MeshTracker mt = new MeshTracker(2);
             Paint paint = new Paint();
             Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(),
diff --git a/tests/Android.mk b/tests/Android.mk
index f86942d..ce9acbd 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -31,7 +31,4 @@
 
 LOCAL_INSTRUMENTATION_FOR := Browser
 
-LOCAL_SDK_VERSION := current
-
 include $(BUILD_PACKAGE)
-
diff --git a/tests/src/com/android/browser/PopularUrlsTest.java b/tests/src/com/android/browser/PopularUrlsTest.java
new file mode 100644
index 0000000..4ae69c7
--- /dev/null
+++ b/tests/src/com/android/browser/PopularUrlsTest.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2010 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 java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.CountDownLatch;
+
+import android.app.Instrumentation;
+import android.content.Intent;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Environment;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebView;
+
+/**
+ *
+ * Iterates over a list of URLs from a file and outputs the time to load each.
+ */
+public class PopularUrlsTest extends ActivityInstrumentationTestCase2<BrowserActivity> {
+
+    private final static String TAG = "PopularUrlsTest";
+    private final static String newLine = System.getProperty("line.separator");
+    private final static String sInputFile = "popular_urls.txt";
+    private final static String sOutputFile = "test_output.txt";
+    private final static File sExternalStorage = Environment.getExternalStorageDirectory();
+    private BrowserActivity mActivity = null;
+    private Instrumentation mInst = null;
+    private CountDownLatch mLatch = new CountDownLatch(1);
+
+    public PopularUrlsTest() {
+        super("com.android.browser", BrowserActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mActivity = getActivity();
+        mInst = getInstrumentation();
+        mInst.waitForIdleSync();
+    }
+
+    static BufferedReader getInputStream() throws FileNotFoundException {
+        String path = sExternalStorage + File.separator + sInputFile;
+        FileReader fileReader = new FileReader(path);
+        BufferedReader bufferedReader = new BufferedReader(fileReader);
+
+        return bufferedReader;
+    }
+
+    static OutputStreamWriter getOutputStream() throws IOException {
+        String path = sExternalStorage + File.separator + sOutputFile;
+
+        File file = new File(path);
+        if (file.exists()) {
+            file.delete();
+        }
+
+        return new FileWriter(file);
+    }
+
+    /**
+     * Gets the browser ready for testing by starting the application
+     * and wrapping the WebView's helper clients.
+     */
+    void setUpBrowser() {
+        Tab tab = mActivity.getTabControl().getCurrentTab();
+        WebView webView = tab.getWebView();
+
+        webView.setWebChromeClient(new TestWebChromeClient(webView.getWebChromeClient()) {
+
+            /**
+             * Reset the latch whenever page progress reaches 100%.
+             */
+            @Override
+            public void onProgressChanged(WebView view, int newProgress) {
+                super.onProgressChanged(view, newProgress);
+                if (newProgress >= 100) {
+                    resetLatch();
+                }
+            }
+
+            /**
+             * Dismisses and logs Javascript alerts.
+             */
+            @Override
+            public boolean onJsAlert(WebView view, String url, String message,
+                    JsResult result) {
+                String logMsg = String.format("JS Alert '%s' received from %s", message, url);
+                Log.w(TAG, logMsg);
+                result.confirm();
+
+                return true;
+            }
+
+            /**
+             * Confirms and logs Javascript alerts.
+             */
+            @Override
+            public boolean onJsConfirm(WebView view, String url, String message,
+                    JsResult result) {
+                String logMsg = String.format("JS Confirmation '%s' received from %s",
+                        message, url);
+                Log.w(TAG, logMsg);
+                result.confirm();
+
+                return true;
+            }
+
+            /**
+             * Confirms and logs Javascript alerts, providing the default value.
+             */
+            @Override
+            public boolean onJsPrompt(WebView view, String url, String message,
+                    String defaultValue, JsPromptResult result) {
+                String logMsg = String.format("JS Prompt '%s' received from %s; " +
+                        "Giving default value '%s'", message, url, defaultValue);
+                Log.w(TAG, logMsg);
+                result.confirm(defaultValue);
+
+                return true;
+            }
+        });
+
+        webView.setWebViewClient(new TestWebViewClient(webView.getWebViewClient()) {
+
+            /**
+             * Bypasses and logs errors.
+             */
+            @Override
+            public void onReceivedError(WebView view, int errorCode,
+                    String description, String failingUrl) {
+                String message = String.format("Error '%s' (%d) loading url: %s",
+                        description, errorCode, failingUrl);
+                Log.w(TAG, message);
+            }
+
+            /**
+             * Ignores and logs SSL errors.
+             */
+            @Override
+            public void onReceivedSslError(WebView view, SslErrorHandler handler,
+                    SslError error) {
+                Log.w(TAG, "SSL error: " + error);
+                handler.proceed();
+            }
+
+        });
+    }
+
+    void resetLatch() {
+        CountDownLatch temp = mLatch;
+        mLatch = new CountDownLatch(1);
+        if (temp != null) {
+            // Notify existing latch that it's done.
+            while (temp.getCount() > 0) {
+                temp.countDown();
+            }
+        }
+    }
+
+    void waitForLoad() throws InterruptedException {
+        mLatch.await();
+    }
+
+    /**
+     * Loops over a list of URLs, points the browser to each one, and records the time elapsed.
+     *
+     * @param input the reader from which to get the URLs.
+     * @param writer the writer to which to output the results.
+     * @throws IOException unable to read from input or write to writer.
+     * @throws InterruptedException the thread was interrupted waiting for the page to load.
+     */
+    void loopUrls(BufferedReader input, OutputStreamWriter writer)
+            throws IOException, InterruptedException {
+        Tab tab = mActivity.getTabControl().getCurrentTab();
+        WebView webView = tab.getWebView();
+        String page;
+
+        while (null != (page = input.readLine())) {
+            Uri uri = Uri.parse(page);
+            webView.clearCache(true);
+            final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+            writer.write(uri.toString());
+
+            long startTime = System.nanoTime();
+            mInst.runOnMainSync(new Runnable() {
+
+                public void run() {
+                    mActivity.onNewIntent(intent);
+                }
+
+            });
+            waitForLoad();
+            long stopTime = System.nanoTime();
+
+            String url = webView.getUrl();
+            Log.i(TAG, "Loaded url: " + url);
+            writer.write("|" + (stopTime - startTime) + newLine);
+        }
+    }
+
+    public void testLoadPerformance() throws IOException, InterruptedException {
+        setUpBrowser();
+
+        OutputStreamWriter writer = getOutputStream();
+        try {
+            BufferedReader bufferedReader = getInputStream();
+            try {
+                loopUrls(bufferedReader, writer);
+            } finally {
+                if (bufferedReader != null) {
+                    bufferedReader.close();
+                }
+            }
+        } finally {
+            if (writer != null) {
+                writer.close();
+            }
+        }
+    }
+}
+
diff --git a/tests/src/com/android/browser/TestWebChromeClient.java b/tests/src/com/android/browser/TestWebChromeClient.java
new file mode 100644
index 0000000..d78eaed
--- /dev/null
+++ b/tests/src/com/android/browser/TestWebChromeClient.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2010 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.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Message;
+import android.view.View;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.JsPromptResult;
+import android.webkit.JsResult;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
+
+/**
+ *
+ * WebChromeClient for browser tests.
+ * Wraps around existing client so that specific methods can be overridden if needed.
+ *
+ */
+abstract class TestWebChromeClient extends WebChromeClient {
+
+    private WebChromeClient mWrappedClient;
+
+    protected TestWebChromeClient(WebChromeClient wrappedClient) {
+        mWrappedClient = wrappedClient;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onProgressChanged(WebView view, int newProgress) {
+        mWrappedClient.onProgressChanged(view, newProgress);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceivedTitle(WebView view, String title) {
+        mWrappedClient.onReceivedTitle(view, title);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceivedIcon(WebView view, Bitmap icon) {
+        mWrappedClient.onReceivedIcon(view, icon);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReceivedTouchIconUrl(WebView view, String url,
+            boolean precomposed) {
+        mWrappedClient.onReceivedTouchIconUrl(view, url, precomposed);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onShowCustomView(View view, CustomViewCallback callback) {
+        mWrappedClient.onShowCustomView(view, callback);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onHideCustomView() {
+        mWrappedClient.onHideCustomView();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onCreateWindow(WebView view, boolean dialog,
+            boolean userGesture, Message resultMsg) {
+        return mWrappedClient.onCreateWindow(view, dialog, userGesture, resultMsg);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onRequestFocus(WebView view) {
+        mWrappedClient.onRequestFocus(view);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onCloseWindow(WebView window) {
+        mWrappedClient.onCloseWindow(window);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onJsAlert(WebView view, String url, String message,
+            JsResult result) {
+        return mWrappedClient.onJsAlert(view, url, message, result);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onJsConfirm(WebView view, String url, String message,
+            JsResult result) {
+        return mWrappedClient.onJsConfirm(view, url, message, result);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onJsPrompt(WebView view, String url, String message,
+            String defaultValue, JsPromptResult result) {
+        return mWrappedClient.onJsPrompt(view, url, message, defaultValue, result);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onJsBeforeUnload(WebView view, String url, String message,
+            JsResult result) {
+        return mWrappedClient.onJsBeforeUnload(view, url, message, result);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onExceededDatabaseQuota(String url, String databaseIdentifier,
+            long currentQuota, long estimatedSize, long totalUsedQuota,
+            WebStorage.QuotaUpdater quotaUpdater) {
+        mWrappedClient.onExceededDatabaseQuota(url, databaseIdentifier, currentQuota,
+                estimatedSize, totalUsedQuota, quotaUpdater);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
+            WebStorage.QuotaUpdater quotaUpdater) {
+        mWrappedClient.onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, quotaUpdater);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onGeolocationPermissionsShowPrompt(String origin,
+            GeolocationPermissions.Callback callback) {
+        mWrappedClient.onGeolocationPermissionsShowPrompt(origin, callback);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void onGeolocationPermissionsHidePrompt() {
+        mWrappedClient.onGeolocationPermissionsHidePrompt();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onJsTimeout() {
+        return mWrappedClient.onJsTimeout();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Deprecated
+    public void onConsoleMessage(String message, int lineNumber, String sourceID) {
+        mWrappedClient.onConsoleMessage(message, lineNumber, sourceID);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+        return mWrappedClient.onConsoleMessage(consoleMessage);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Bitmap getDefaultVideoPoster() {
+        return mWrappedClient.getDefaultVideoPoster();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public View getVideoLoadingProgressView() {
+        return mWrappedClient.getVideoLoadingProgressView();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void getVisitedHistory(ValueCallback<String[]> callback) {
+        mWrappedClient.getVisitedHistory(callback);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void openFileChooser(ValueCallback<Uri> uploadFile) {
+        mWrappedClient.openFileChooser(uploadFile);
+    }
+}
diff --git a/tests/src/com/android/browser/TestWebViewClient.java b/tests/src/com/android/browser/TestWebViewClient.java
new file mode 100644
index 0000000..7159a7e
--- /dev/null
+++ b/tests/src/com/android/browser/TestWebViewClient.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2010 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.graphics.Bitmap;
+import android.net.http.SslError;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.webkit.HttpAuthHandler;
+import android.webkit.SslErrorHandler;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+/**
+ *
+ *
+ * WebViewClient for browser tests.
+ * Wraps around existing client so that specific methods can be overridden if needed.
+ *
+ */
+abstract class TestWebViewClient extends WebViewClient {
+
+  private WebViewClient mWrappedClient;
+
+  protected TestWebViewClient(WebViewClient wrappedClient) {
+    mWrappedClient = wrappedClient;
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean shouldOverrideUrlLoading(WebView view, String url) {
+      return mWrappedClient.shouldOverrideUrlLoading(view, url);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onPageStarted(WebView view, String url, Bitmap favicon) {
+    mWrappedClient.onPageStarted(view, url, favicon);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onPageFinished(WebView view, String url) {
+    mWrappedClient.onPageFinished(view, url);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onLoadResource(WebView view, String url) {
+    mWrappedClient.onLoadResource(view, url);
+  }
+
+  /** {@inheritDoc} */
+  @Deprecated
+  @Override
+  public void onTooManyRedirects(WebView view, Message cancelMsg,
+          Message continueMsg) {
+      mWrappedClient.onTooManyRedirects(view, cancelMsg, continueMsg);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onReceivedError(WebView view, int errorCode,
+          String description, String failingUrl) {
+    mWrappedClient.onReceivedError(view, errorCode, description, failingUrl);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onFormResubmission(WebView view, Message dontResend,
+          Message resend) {
+    mWrappedClient.onFormResubmission(view, dontResend, resend);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void doUpdateVisitedHistory(WebView view, String url,
+          boolean isReload) {
+    mWrappedClient.doUpdateVisitedHistory(view, url, isReload);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onReceivedSslError(WebView view, SslErrorHandler handler,
+          SslError error) {
+      mWrappedClient.onReceivedSslError(view, handler, error);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onReceivedHttpAuthRequest(WebView view,
+          HttpAuthHandler handler, String host, String realm) {
+      mWrappedClient.onReceivedHttpAuthRequest(view, handler, host, realm);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
+      return mWrappedClient.shouldOverrideKeyEvent(view, event);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+    mWrappedClient.onUnhandledKeyEvent(view, event);
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public void onScaleChanged(WebView view, float oldScale, float newScale) {
+    mWrappedClient.onScaleChanged(view, oldScale, newScale);
+  }
+}