resolved conflicts for merge of a34f6861 to master
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9fdf63b..f3707d4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,25 +1,25 @@
 <!--
-/* //device/apps/Browser/AndroidManifest.xml
-**
-** Copyright 2006, 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.
-*/
+/*
+ * Copyright 2006, 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.
+ */
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.browser">
 
     <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"/>
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -34,6 +34,7 @@
     <application   android:name="Browser"
                    android:label="@string/application_name"
                    android:icon="@drawable/ic_launcher_browser"
+                   android:backupAgent=".BrowserBackupAgent"
                    android:taskAffinity="android.task.browser" >
 
         <provider android:name="BrowserProvider"
@@ -120,7 +121,6 @@
         <activity android:name="BrowserBookmarksPage" android:label="@string/bookmarks"
                   android:launchMode="singleTop" android:configChanges="orientation|keyboardHidden">
         </activity>
-
         <activity android:name="MostVisitedActivity" android:label=""
                 android:launchMode="singleTop" android:configChanges="orientation|keyboardHidden"/>
 
@@ -152,11 +152,20 @@
                   android:configChanges="orientation|keyboardHidden">
         </activity>
 
+        <activity android:name="WebsiteSettingsActivity" android:label=""
+                  android:configChanges="orientation|keyboardHidden">
+        </activity>
+
         <activity android:name="GearsDialog" android:process=":dialog"
                   android:configChanges="orientation|keyboardHidden"
                   android:theme="@android:style/Theme.Dialog">
         </activity>
 
+        <activity android:name="PermissionDialog"
+                  android:configChanges="orientation|keyboardHidden"
+                  android:theme="@android:style/Theme.Dialog">
+        </activity>
+
         <activity android:name="GearsNativeDialog"
                   android:configChanges="orientation|keyboardHidden"
                   android:theme="@android:style/Theme.Dialog">
diff --git a/res/drawable/background_titlebar.png b/res/drawable/background_titlebar.png
new file mode 100644
index 0000000..1fdb078
--- /dev/null
+++ b/res/drawable/background_titlebar.png
Binary files differ
diff --git a/res/drawable/blank.xml b/res/drawable/blank.xml
new file mode 100644
index 0000000..724f5e7
--- /dev/null
+++ b/res/drawable/blank.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape>
+            <corners android:radius="5dip"/>
+            <solid android:color="@color/white"/>
+        </shape>
+    </item>
+</layer-list>
diff --git a/res/drawable/button_line.xml b/res/drawable/button_line.xml
new file mode 100644
index 0000000..67869d0
--- /dev/null
+++ b/res/drawable/button_line.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+    <item>
+        <shape android:width="2dip">
+            <solid android:color="#ff000000"/>
+        </shape>
+    </item>
+    <item android:left="1dip" android:width="1dip">
+        <shape>
+            <solid android:color="#ffffffff"/>
+        </shape>
+    </item>
+</layer-list>
diff --git a/res/drawable/ic_titlebar_zoom.png b/res/drawable/ic_titlebar_zoom.png
new file mode 100644
index 0000000..b497dab
--- /dev/null
+++ b/res/drawable/ic_titlebar_zoom.png
Binary files differ
diff --git a/res/drawable/ic_titlebar_zoom_out.png b/res/drawable/ic_titlebar_zoom_out.png
new file mode 100644
index 0000000..4b5a3dd
--- /dev/null
+++ b/res/drawable/ic_titlebar_zoom_out.png
Binary files differ
diff --git a/res/layout/application.xml b/res/layout/application.xml
new file mode 100644
index 0000000..d8de265
--- /dev/null
+++ b/res/layout/application.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:drawingCacheQuality="auto"
+  android:layout_width="fill_parent"
+  android:layout_height="wrap_content"
+  android:gravity="center"
+  android:padding="0dip">
+
+  <ImageView android:id="@+id/icon"
+    android:layout_width="32px"
+    android:layout_height="32px"
+    android:padding="2dip" />
+
+  <TwoLineListItem
+    android:paddingTop="2dip"
+    android:paddingBottom="2dip"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:minHeight="?android:attr/listPreferredItemHeight"
+    android:mode="twoLine">
+
+    <TextView android:id="@+id/title"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:layout_marginLeft="6dip"
+      android:layout_marginTop="6dip"
+      android:textAppearance="?android:attr/textAppearanceLarge"/>
+
+    <TextView android:id="@+id/subtitle"
+      android:layout_width="fill_parent"
+      android:layout_height="wrap_content"
+      android:layout_below="@+id/title"
+      android:layout_alignLeft="@+id/title"
+      android:textAppearance="?android:attr/textAppearanceSmall"/>
+
+  </TwoLineListItem>
+
+</LinearLayout>
diff --git a/res/layout/bookmark_thumbnail.xml b/res/layout/bookmark_thumbnail.xml
new file mode 100644
index 0000000..c1e0cd2
--- /dev/null
+++ b/res/layout/bookmark_thumbnail.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:orientation="vertical"
+    android:padding="4dip"
+    >
+
+    <ImageView android:id="@+id/thumb"
+        android:src="@drawable/blank"
+        android:scaleType="center"
+        android:layout_width="100dip"
+        android:layout_height="80dip"
+        />
+
+    <ImageView android:id="@+id/fav"
+        android:scaleType="fitCenter"
+        android:layout_width="20dip"
+        android:layout_height="20dip"
+        android:padding="2dip"
+        android:layout_alignBottom="@+id/thumb"
+        android:background="@drawable/fav_icn_background"
+        />
+    <LinearLayout android:id="@+id/holder"
+        android:layout_height="wrap_content"
+        android:layout_width="fill_parent"
+        android:orientation="horizontal"
+        android:background="#cc000000"
+        android:gravity="center"
+        android:layout_alignBottom="@+id/thumb"
+        >
+        <ImageView
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:layout_marginTop="3dip"
+            android:layout_marginBottom="3dip"
+            android:src="@drawable/ic_tab_browser_bookmark_selected"
+            />
+        <TextView
+            android:layout_height="wrap_content"
+            android:layout_width="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textStyle="bold"
+            android:textColor="@color/white"
+            android:text="@string/add_bookmark_short"
+            />
+    </LinearLayout>
+    <!-- Keep the width in sync with BrowserBookmarksPage and
+            BrowserActivity.updateScreenshot -->
+    <TextView android:id="@+id/label"
+        android:layout_width="100dip"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/thumb"
+        android:layout_gravity="bottom"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textStyle="bold"
+        android:textColor="@color/white"
+        android:maxLines="1"
+        android:paddingTop="3dip"
+        android:paddingBottom="3dip"
+        android:paddingLeft="2dip"
+        android:paddingRight="2dip"
+        android:scrollHorizontally="true"
+        android:ellipsize="marquee"
+        />
+</RelativeLayout>
diff --git a/res/layout/browser_downloads_page.xml b/res/layout/browser_downloads_page.xml
index c83a727..1d4d4e6 100644
--- a/res/layout/browser_downloads_page.xml
+++ b/res/layout/browser_downloads_page.xml
@@ -18,8 +18,14 @@
 ** limitations under the License.
 */
 -->
-
-<ListView xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/list"
-    android:layout_width="fill_parent"
-    android:layout_height="fill_parent"/>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <ListView 
+        android:id="@+id/list"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"/>
+    <ViewStub 
+        android:id="@+id/empty"
+        android:layout="@layout/no_downloads"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"/>
+</merge>
diff --git a/res/layout/custom_screen.xml b/res/layout/custom_screen.xml
new file mode 100644
index 0000000..3ea8ec9
--- /dev/null
+++ b/res/layout/custom_screen.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android">
+    <FrameLayout android:id="@+id/fullscreen_custom_content"
+        android:visibility="gone"
+        android:background="@color/black"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"
+    />
+    <LinearLayout android:orientation="vertical"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent">
+        <com.android.browser.TitleBar android:id="@+id/title_bar"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+        />
+
+        <LinearLayout android:id="@+id/error_console"
+            android:layout_width="fill_parent"
+            android:layout_height="wrap_content"
+        />
+
+        <FrameLayout android:id="@+id/main_content"
+            android:layout_width="fill_parent"
+            android:layout_height="fill_parent"
+            android:foreground="?android:attr/windowContentOverlay"
+        />
+    </LinearLayout>
+</FrameLayout>
diff --git a/res/layout/error_console.xml b/res/layout/error_console.xml
new file mode 100644
index 0000000..0fffcde
--- /dev/null
+++ b/res/layout/error_console.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- Copyright (C) 2009 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/error_console_view_group_id"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent"
+    android:orientation="vertical"
+    android:background="#000000">
+
+    <TextView android:id="@+id/error_console_header_id"
+        android:text="@string/error_console_header_text_minimized"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:paddingTop="2dip"
+        android:paddingBottom="2dip"
+        android:paddingLeft="5dip"
+        style="?android:attr/listSeparatorTextViewStyle"
+        android:visibility="gone"
+    />
+
+    <view class="com.android.browser.ErrorConsoleView$ErrorConsoleListView"
+        android:id="@+id/error_console_list_id"
+        android:layout_width="fill_parent"
+        android:layout_height="200dip"
+        android:visibility="gone"
+        android:layout_weight="1"
+        android:cacheColorHint="#000000"
+    />
+
+    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/error_console_eval_view_group_id"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone">
+
+        <EditText android:id="@+id/error_console_eval_text_id"
+            android:hint="@string/error_console_eval_text_hint"
+            android:layout_height="wrap_content"
+            android:scrollHorizontally="true"
+            android:inputType="text"
+            android:layout_width="0dip"
+            android:layout_gravity="left"
+            android:layout_weight="1.0"
+        />
+
+        <Button android:id="@+id/error_console_eval_button_id"
+            android:text="@string/error_console_eval_button_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+        />
+    </LinearLayout>
+</LinearLayout>
diff --git a/res/layout/permission_dialog.xml b/res/layout/permission_dialog.xml
new file mode 100644
index 0000000..ff24eaf
--- /dev/null
+++ b/res/layout/permission_dialog.xml
@@ -0,0 +1,158 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 2009, 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.
+ */
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+  android:drawingCacheQuality="auto"
+  android:layout_width="fill_parent"
+  android:layout_height="wrap_content"
+  android:orientation="vertical"
+  android:padding="0dip">
+
+  <LinearLayout
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:padding="10dip">
+
+    <ImageView
+      android:id="@+id/icon"
+      android:paddingRight="10dip"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_centerHorizontal="true"/>
+
+    <TextView
+      android:id="@+id/dialog_title"
+      android:layout_width="wrap_content"
+      android:layout_height="wrap_content"
+      android:layout_centerVertical="true"
+      android:gravity="center_vertical"
+      android:visibility="gone"
+      android:textSize="16dip"
+      android:textStyle="bold"
+      android:textColor="@color/white"/>
+
+  </LinearLayout>
+
+  <LinearLayout
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <ImageView android:id="@+id/titleDivider"
+        android:layout_width="fill_parent"
+        android:layout_height="1dip"
+        android:scaleType="fitXY"
+        android:gravity="fill_horizontal"
+        android:src="@drawable/dialog_divider_horizontal_light"
+        android:layout_marginLeft="10dip"
+        android:layout_marginRight="10dip"/>
+
+  </LinearLayout>
+
+  <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="fill_parent"
+    android:layout_height="wrap_content"
+    android:paddingBottom="10dip"
+    android:layout_weight="1">
+
+    <LinearLayout
+      android:orientation="vertical"
+      android:layout_width="fill_parent"
+      android:paddingTop="10dip"
+      android:paddingLeft="10dip"
+      android:paddingRight="10dip"
+      android:layout_height="wrap_content">
+
+      <TextView
+        android:id="@+id/origin"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:textStyle="bold"
+        android:gravity="left"
+        android:textSize="16dip"
+        android:textColor="@color/white"/>
+
+      <TextView
+        android:id="@+id/dialog_message"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        android:paddingLeft="4dip"
+        android:paddingTop="10dip"
+        android:gravity="left"
+        android:textSize="16dip"
+        android:textColor="@color/white"/>
+
+    </LinearLayout>
+
+  </ScrollView>
+
+  <LinearLayout
+    android:layout_width="fill_parent"
+    android:background="@color/gray"
+    android:layout_height="wrap_content"
+    android:paddingTop="4dip"
+    android:paddingLeft="0dip"
+    android:paddingRight="0dip">
+
+    <LinearLayout android:id="@+id/leftSpacer"
+       android:layout_weight="0.25"
+       android:layout_width="0dip"
+       android:layout_height="wrap_content"
+       android:orientation="horizontal"
+       android:visibility="gone"/>
+
+    <Button
+      android:id="@+id/button_allow"
+      android:layout_width="96dip"
+      android:layout_height="48dip"
+      android:layout_gravity="left"
+      android:layout_weight="1"
+      android:maxLines="2"
+      android:textSize="13dip"/>
+
+    <Button
+      android:id="@+id/button_alwaysdeny"
+      android:layout_width="96dip"
+      android:layout_height="48dip"
+      android:layout_gravity="left"
+      android:layout_weight="1"
+      android:maxLines="2"
+      android:textSize="13dip"/>
+
+    <Button
+      android:id="@+id/button_deny"
+      android:layout_width="96dip"
+      android:layout_height="48dip"
+      android:layout_gravity="right"
+      android:layout_weight="1"
+      android:maxLines="2"
+      android:textSize="13dip"/>
+
+    <LinearLayout android:id="@+id/rightSpacer"
+      android:layout_width="0dip"
+      android:layout_weight="0.25"
+      android:layout_height="wrap_content"
+      android:orientation="horizontal"
+      android:visibility="gone" />
+
+  </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/title_bar.xml b/res/layout/title_bar.xml
new file mode 100644
index 0000000..0f70519
--- /dev/null
+++ b/res/layout/title_bar.xml
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2009 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:fitsSystemWindows="true"
+    android:orientation="horizontal"
+    android:layout_width="fill_parent"
+    android:background="@drawable/background_titlebar"
+    android:layout_height="45dip">
+    <ImageView android:id="@+id/favicon"
+        android:layout_height="20dip"
+        android:layout_width="20dip"
+        android:layout_marginLeft="6dip"
+        android:layout_marginRight="6dip"
+        android:layout_gravity="center_vertical"
+        />
+    <!-- layout which contains the title, progress bar, and url -->
+    <LinearLayout
+        android:orientation="vertical"
+        android:layout_width="0dip"
+        android:layout_weight="1"
+        android:layout_height="wrap_content">
+        <!-- This part contains the favicon and the progress bar -->
+        <RelativeLayout
+            android:layout_marginTop="3dip"
+            android:layout_height="wrap_content"
+            android:layout_width="fill_parent"
+            >
+            <LinearLayout
+                android:orientation="horizontal"
+                android:layout_marginTop="3dip"
+                android:layout_width="fill_parent"
+                android:layout_height="wrap_content">
+                <ProgressBar android:id="@+id/progress_horizontal"
+                    style="?android:attr/progressBarStyleHorizontal"
+                    android:layout_width="0dip"
+                    android:layout_weight="1"
+                    android:layout_height="wrap_content"
+                    android:max="100" />
+                <ProgressBar android:id="@+id/progress_circular"
+                    style="?android:attr/progressBarStyleSmallTitle"
+                    android:layout_marginLeft="3dip"
+                    android:layout_gravity="center_vertical"
+                    android:max="100"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content" />
+            </LinearLayout>
+            <!-- need to make this no wider than the horizontal progress bar -->
+            <TextView android:id="@+id/title"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_marginLeft="6dip"
+                android:layout_marginTop="4dip"
+                android:textSize="14dip"
+                android:textColor="@color/white"
+                android:textStyle="bold"
+                android:singleLine="true"
+                />
+            <TextView android:id="@+id/url"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="6dip"
+                android:layout_below="@id/title"
+                android:textSize="12dip"
+                android:textColor="@color/white"
+                android:singleLine="true"
+                />
+            <ImageView android:id="@+id/lock_icon"
+                android:layout_height="wrap_content"
+                android:layout_width="wrap_content"
+                android:layout_alignBottom="@id/title"
+                android:layout_alignParentRight="true"
+                android:visibility="gone"/>
+        </RelativeLayout>
+    </LinearLayout>
+    <!-- These buttons will change look/functionality -->
+    <ImageView android:id="@+id/lft_button"
+        android:layout_width="52dip"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:src="@android:drawable/btn_star"/>
+    <!-- divider -->
+    <View android:id="@+id/divider"
+        android:layout_height="fill_parent"
+        android:layout_width="2dip"
+        android:background="@drawable/button_line"/>
+    <ImageView android:id="@+id/rt_button"
+        android:layout_width="52dip"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:src="@*android:drawable/btn_browser_zoom_page_overview"/>
+</LinearLayout>
diff --git a/res/menu/bookmarks.xml b/res/menu/bookmarks.xml
index 50dcfa9..8a6e32e 100644
--- a/res/menu/bookmarks.xml
+++ b/res/menu/bookmarks.xml
@@ -18,4 +18,6 @@
     <item android:id="@+id/new_context_menu_id"
         android:icon="@android:drawable/ic_menu_add"
         android:title="@string/bookmark_page" />
+    <item android:id="@+id/switch_mode_menu_id"
+        android:title="@string/switch_mode"/>
 </menu>
diff --git a/res/menu/bookmarkscontext.xml b/res/menu/bookmarkscontext.xml
index b43e242..ba5d1dc 100644
--- a/res/menu/bookmarkscontext.xml
+++ b/res/menu/bookmarkscontext.xml
@@ -34,5 +34,7 @@
       android:title="@string/contextmenu_copylink"/>
     <item android:id="@+id/delete_context_menu_id"
       android:title="@string/remove_bookmark"/>
+    <item android:id="@+id/homepage_context_menu_id"
+      android:title="@string/set_as_homepage"/>
   </group>
 </menu>
diff --git a/res/menu/historycontext.xml b/res/menu/historycontext.xml
index dfda010..5306396 100644
--- a/res/menu/historycontext.xml
+++ b/res/menu/historycontext.xml
@@ -26,5 +26,7 @@
     <item android:id="@+id/copy_context_menu_id"
         android:title="@string/contextmenu_copylink"/> 
     <item android:id="@+id/delete_context_menu_id"
-        android:title="@string/remove_history_item"/> 
+        android:title="@string/remove_history_item"/>
+    <item android:id="@+id/homepage_context_menu_id"
+        android:title="@string/set_as_homepage"/>
 </menu>
diff --git a/res/values/arrays.xml b/res/values/arrays.xml
new file mode 100644
index 0000000..219cc6b
--- /dev/null
+++ b/res/values/arrays.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2009 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.
+  -->
+<resources>
+   <string-array name="webstorage_quota_entries">
+      <item>No quota allowed</item>
+      <item>5 MB</item>
+      <item>10 MB</item>
+      <item>30 MB</item>
+      <item>100 MB</item>
+   </string-array>
+   <string-array name="webstorage_quota_entries_values">
+      <item>0</item>
+      <item>5</item>
+      <item>10</item>
+      <item>30</item>
+      <item>100</item>
+   </string-array>
+</resources>
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 69c17cb..c600666 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -58,7 +58,10 @@
         <!-- Case of several matches -->
         <item quantity="other"><xliff:g id="number" example="137">%d</xliff:g> matches</item>
     </plurals>
-    
+
+    <!-- Displayed on the title bar while the page is loading -->
+    <string name="title_bar_loading">Loading\u2026</string>
+
     <!-- Menu item -->
     <string name="page_info">Page info</string>
     <!-- Label for a button on an SSL error dialog that allows the user to see
@@ -141,10 +144,16 @@
     <string name="open_bookmark">Open</string>
     <!-- Menu item to remove the currently highlighted bookmark-->
     <string name="remove_bookmark">Delete bookmark</string>
+    <!-- Context menu item to remove a history item from bookmarks -->
+    <string name="remove_from_bookmarks">Remove from bookmarks</string>
     <!-- Menu item to remove the currently highlighted history entry from the list of previously visited sites -->
     <string name="remove_history_item">Remove from history</string>
+    <!-- Context menu item for setting the bookmark/history item as the homepage -->
+    <string name="set_as_homepage">Set as homepage</string>
     <!-- Toast informing the user that their action to save a bookmark has succeeded -->
     <string name="bookmark_saved">Saved to bookmarks.</string>
+    <!-- Toast confirming that the homepage has been set -->
+    <string name="homepage_set">Homepage set</string>
     <!-- Error that appears in the title of Bookmark dialog when user selects OK with empty Name field -->
     <string name="bookmark_needs_title">"Bookmark must have a name."</string>
         <!-- Error that appears in the title of Bookmark dialog when user selects OK with empty Location field -->
@@ -160,6 +169,9 @@
             currently on, but is not visible because the bookmarks page is 
             showing. -->
     <string name="bookmark_page">Bookmark last-viewed page</string>
+    <!-- Menu item in the page that displays all bookmarks.  Switches between
+            a vertical list view and a grid view which shows thumbnails -->
+    <string name="switch_mode">Switch viewing mode</string>
     <!-- Summary text under the New Bookmark item on the Bookmarks screen.
             Tells the user that if they select this item, it will bring up a
             dialog to bookmark the page that the browser is currently viewing,
@@ -284,14 +296,18 @@
     <string name="pref_content_autofit">Auto-fit pages</string>
     <!-- Settings summary -->
     <string name="pref_content_autofit_summary">Format Web pages to fit the screen</string>
+    <!-- Settings label for enabling a mode where the browser is always set to landscape mode -->
+    <string name="pref_content_landscape_only">Landscape only display</string>
+    <!-- Settings summary -->
+    <string name="pref_content_landscape_only_summary">Always read pages in the wider, landscape screen orientation</string>
     <!-- Settings screen, section title -->
     <string name="pref_privacy_title">Privacy settings</string>
     <!-- Settings label -->
     <string name="pref_privacy_clear_cache">Clear cache</string>
     <!-- Settings summary -->
-    <string name="pref_privacy_clear_cache_summary">Delete all cached page content</string>
+    <string name="pref_privacy_clear_cache_summary">Clear locally cached content and databases</string>
     <!-- Confirmation dialog message -->
-    <string name="pref_privacy_clear_cache_dlg">The cache will be cleared.</string>
+    <string name="pref_privacy_clear_cache_dlg">Locally cached content and databases will be cleared.</string>
     <!-- Settings label -->
     <string name="pref_privacy_clear_cookies">Clear all cookie data</string>
     <!-- Settings summary -->
@@ -316,6 +332,16 @@
     <string name="pref_privacy_clear_passwords_summary">Clear all the saved passwords</string>
     <!-- Confirmation dialog message -->
     <string name="pref_privacy_clear_passwords_dlg">All saved passwords will be cleared.</string>
+    <!-- Settings label -->
+    <string name="pref_privacy_enable_geolocation">Enable location</string>
+    <!-- Settings summary -->
+    <string name="pref_privacy_enable_geolocation_summary">Allow sites to request access to your location</string>
+    <!-- Settings label -->
+    <string name="pref_privacy_clear_geolocation_access">Clear location access</string>
+    <!-- Settings summary -->
+    <string name="pref_privacy_clear_geolocation_access_summary">Clear location access for all websites</string>
+    <!-- Confirmation dialog message -->
+    <string name="pref_privacy_clear_geolocation_access_dlg">Clear location access for all websites</string>
     <!-- Settings screen, section title -->
     <string name="pref_security_title">Security settings</string>
     <!-- Settings label -->
@@ -377,6 +403,10 @@
     <!-- Settings summary -->
     <string name="pref_extras_gears_enable_summary">Applications that extend browser functionality</string>
     <!-- Settings label -->
+    <string name="pref_extras_website_settings">Website settings</string>
+    <!-- Settings summary -->
+    <string name="pref_extras_website_settings_summary">View advanced settings for individual websites</string>
+    <!-- Settings label -->
     <string name="pref_extras_gears_settings">Gears settings</string>
     <!-- Settings summary -->
     <string name="pref_plugin_installed">Plugins list</string>
@@ -404,6 +434,8 @@
     <!-- Do not tranlsate.  Development option -->
     <string name="pref_development_nav_dump" translatable="false">Enable nav cache dump</string>
     <!-- Do not tranlsate.  Development option -->
+    <string name="js_engine_flags" translatable="false">Set JS flags</string>
+    <!-- Do not tranlsate.  Development option -->
     <string name="pref_development_uastring" translatable="false">UAString</string>
     <!-- Do not tranlsate.  Development option -->
     <string-array name="pref_development_ua_choices" translatable="false">
@@ -417,6 +449,7 @@
         <item>1</item>
         <item>2</item>
     </string-array>
+    <string name="pref_development_error_console" translatable="false">Show JavaScript Console</string>
     <!-- Settings screen, setting option name -->
     <string name="pref_default_text_encoding">Text encoding</string>
     <!-- Options in the Default encoding dialog box -->
@@ -465,6 +498,9 @@
     <string name="empty_history">Browser history is empty.</string>
     <!-- Displayed at the top of the bookmarks page.  When clicked, it bookmarks the page the browser is currently showing -->
     <string name="add_new_bookmark">Add bookmark\u2026</string>
+    <!-- Verb placed in front of a screenshot of a web page that, when clicked,
+            will add that page to bookmarks -->
+    <string name="add_bookmark_short">Add</string>
     <!-- Add bookmark dialog sets its title to this if we have no database.
             This is an error case -->
     <string name="no_database">No database!</string>
@@ -474,7 +510,7 @@
     <string name="search_label" translatable="false">Browser</string>
     <!-- This string is for the browser "Go To" UI. -->
     <!-- This is the hint text shown in the Go To widget, before text is entered. -->
-    <string name="search_hint">Type Web address</string>
+    <string name="search_hint">Search or type URL</string>
     <!-- This string is for the browser "Go To" UI. -->
     <!-- This is the button label in the "Go To" UI. -->
     <string name="search_button_text">Go</string>
@@ -671,6 +707,9 @@
     <!-- Gears Dialogs -->
     <string name="query_data_prompt">Allow storage</string>
     <string name="query_data_message">This web site would like to store information on your phone.</string>
+    <string name="query_storage_quota_prompt">Increase storage quota</string>
+    <string name="query_storage_quota_message">This web site is over its current
+      storage limit. Would you like to increase its quota limit ?</string>
     <string name="location_prompt">Access your location</string>
     <string name="location_message">This web site would like to have access to your location.</string>
     <string name="shortcut_prompt">Create a shortcut</string>
@@ -727,7 +766,39 @@
     <string name="unrecognized_dialog_message">Unrecognized dialog type</string>
     <string name="default_button">OK</string>
 
+    <!-- HTML5 dialogs -->
+    <!-- Used as a status bar notification when the browser is running out of space trying to allocate or increase the database quota for an HTML5 databae or application cache. -->
+    <string name="webstorage_outofspace_notification_title">Browser storage full</string>
+    <string name="webstorage_outofspace_notification_text">Click to free up space.</string>
+    <!-- Used in the Browser Settings -->
+    <string name="webstorage_clear_data_title">Clear stored data</string>
+    <string name="webstorage_clear_data_summary">Remove all databases associated with this website</string>
+    <!-- Confirmation dialog when the user ask to clear all data for an origin -->
+    <string name="webstorage_clear_data_dialog_title">Clear stored data</string>
+    <string name="webstorage_clear_data_dialog_message">All data stored by this website will be deleted</string>
+    <string name="webstorage_clear_data_dialog_ok_button">Clear All</string>
+    <string name="webstorage_clear_data_dialog_cancel_button">Cancel</string>
+    <!-- Strings used in the summary of origins -->
+    <string name="webstorage_origin_summary_mb_stored">MB stored on your phone</string>
+
+    <!-- Geolocation -->
+    <!-- Settings page, Advanced Settings -> Website settings -> <origin> -->
+    <string name="geolocation_settings_page_title">Clear location access</string>
+    <string name="geolocation_settings_page_summary_allowed">This site can currently access your location</string>
+    <string name="geolocation_settings_page_summary_not_allowed">This site cannot currently access your location</string>
+    <!-- Settings page dialog -->
+    <string name="geolocation_settings_page_dialog_title">Clear location access</string>
+    <string name="geolocation_settings_page_dialog_message">Location access for this website will be cleared</string>
+    <string name="geolocation_settings_page_dialog_ok_button">Clear access</string>
+    <string name="geolocation_settings_page_dialog_cancel_button">Cancel</string>
+
     <!-- Zoom-related strings --><skip />
     <!-- Caption for a button that is shown when the zoom widget is showing.  The button's action will switch to the zoom overview mode. -->
     <string name="zoom_overview_button_text">Overview</string>
+
+    <string name="error_console_header_text_minimized" translatable="false">Show JavaScript Console</string>
+    <string name="error_console_header_text_maximized" translatable="false">JavaScript Console</string>
+    <string name="error_console_eval_text_hint" translatable="false">Evaluate JavaScript</string>
+    <string name="error_console_eval_button_text" translatable="false">Evaluate</string>
+
 </resources>
diff --git a/res/xml/browser_preferences.xml b/res/xml/browser_preferences.xml
index 23618c5..83c492e 100644
--- a/res/xml/browser_preferences.xml
+++ b/res/xml/browser_preferences.xml
@@ -62,6 +62,12 @@
                 android:summary="@string/pref_content_autofit_summary" />  
 
         <CheckBoxPreference
+                android:key="landscape_only"
+                android:defaultValue="false"
+                android:title="@string/pref_content_landscape_only"
+                android:summary="@string/pref_content_landscape_only_summary" />
+
+        <CheckBoxPreference
                 android:key="enable_javascript"
                 android:defaultValue="true"
                 android:title="@string/pref_content_javascript" />     
@@ -128,6 +134,21 @@
                 android:dialogTitle="@string/clear" 
                 android:dialogIcon="@android:drawable/ic_dialog_alert"/>
 
+        <CheckBoxPreference
+                android:key="enable_geolocation"
+                android:defaultValue="true"
+                android:title="@string/pref_privacy_enable_geolocation"
+                android:summary="@string/pref_privacy_enable_geolocation_summary" />
+
+        <com.android.browser.BrowserYesNoPreference
+                android:key="privacy_clear_geolocation_access"
+                android:dependency="enable_geolocation"
+                android:title="@string/pref_privacy_clear_geolocation_access"
+                android:summary="@string/pref_privacy_clear_geolocation_access_summary"
+                android:dialogMessage="@string/pref_privacy_clear_geolocation_access_dlg"
+                android:dialogTitle="@string/clear"
+                android:dialogIcon="@android:drawable/ic_dialog_alert"/>
+
     </PreferenceCategory>
     
         <PreferenceCategory
@@ -173,6 +194,11 @@
                     android:title="@string/pref_extras_gears_settings"
                     android:summary="@string/pref_extras_gears_settings_summary" />
 
+            <PreferenceScreen
+                  android:key="website_settings"
+                  android:title="@string/pref_extras_website_settings"
+                  android:summary="@string/pref_extras_website_settings_summary" />
+
             <com.android.browser.BrowserYesNoPreference
                     android:key="reset_default_preferences"
                     android:title="@string/pref_extras_reset_default"
diff --git a/res/xml/debug_preferences.xml b/res/xml/debug_preferences.xml
index 22b5997..c1ed1e6 100644
--- a/res/xml/debug_preferences.xml
+++ b/res/xml/debug_preferences.xml
@@ -19,6 +19,13 @@
     <PreferenceCategory
         android:title="@string/pref_development_title" 
         android:key="debug_menu" >
+
+        <!-- The javascript console is enabled by default when the user has
+             also enabled debug mode by navigating to about:debug. -->
+        <CheckBoxPreference
+            android:key="javascript_console"
+            android:defaultValue="true"
+            android:title="@string/pref_development_error_console" />
                         
         <CheckBoxPreference
             android:key="small_screen"
@@ -50,6 +57,11 @@
             android:defaultValue="false"
             android:title="@string/pref_development_nav_dump" />
 
+        <EditTextPreference
+            android:key="js_engine_flags"
+            android:title="@string/js_engine_flags"
+            android:singleLine="true" />
+
         <ListPreference
             android:key="user_agent"
             android:title="@string/pref_development_uastring"
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
index cf3fe70..191659a 100644
--- a/src/com/android/browser/AddBookmarkPage.java
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -18,18 +18,14 @@
 
 import android.app.Activity;
 import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.database.Cursor;
 import android.net.ParseException;
 import android.net.WebAddress;
 import android.os.Bundle;
 import android.provider.Browser;
 import android.view.View;
 import android.view.Window;
-import android.webkit.WebIconDatabase;
 import android.widget.EditText;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -46,11 +42,6 @@
     private View        mCancelButton;
     private boolean     mEditingExisting;
     private Bundle      mMap;
-    
-    private static final String[]   mProjection = 
-        { "_id", "url", "bookmark", "created", "title", "visits" };
-    private static final String     WHERE_CLAUSE = "url = ?";
-    private final String[]          SELECTION_ARGS = new String[1];
 
     private View.OnClickListener mSaveBookmark = new View.OnClickListener() {
         public void onClick(View v) {
@@ -151,70 +142,7 @@
                 setResult(RESULT_OK, (new Intent()).setAction(
                         getIntent().toString()).putExtras(mMap));
             } else {
-                // Want to append to the beginning of the list
-                long creationTime = new Date().getTime();
-                SELECTION_ARGS[0] = url;
-                ContentResolver cr = getContentResolver();
-                Cursor c = cr.query(Browser.BOOKMARKS_URI,
-                        mProjection,
-                        WHERE_CLAUSE,
-                        SELECTION_ARGS,
-                        null);
-                ContentValues map = new ContentValues();
-                if (c.moveToFirst() && c.getInt(c.getColumnIndexOrThrow(
-                        Browser.BookmarkColumns.BOOKMARK)) == 0) {
-                    // This means we have been to this site but not bookmarked
-                    // it, so convert the history item to a bookmark                    
-                    map.put(Browser.BookmarkColumns.CREATED, creationTime);
-                    map.put(Browser.BookmarkColumns.TITLE, title);
-                    map.put(Browser.BookmarkColumns.BOOKMARK, 1);
-                    cr.update(Browser.BOOKMARKS_URI, map, 
-                            "_id = " + c.getInt(0), null);
-                } else {
-                    int count = c.getCount();
-                    boolean matchedTitle = false;
-                    for (int i = 0; i < count; i++) {
-                        // One or more bookmarks already exist for this site.
-                        // Check the names of each
-                        c.moveToPosition(i);
-                        if (c.getString(c.getColumnIndexOrThrow(
-                                Browser.BookmarkColumns.TITLE)).equals(title)) {
-                            // The old bookmark has the same name.
-                            // Update its creation time.
-                            map.put(Browser.BookmarkColumns.CREATED,
-                                    creationTime);
-                            cr.update(Browser.BOOKMARKS_URI, map, 
-                                    "_id = " + c.getInt(0), null);
-                            matchedTitle = true;
-                        }
-                    }
-                    if (!matchedTitle) {
-                        // Adding a bookmark for a site the user has visited,
-                        // or a new bookmark (with a different name) for a site
-                        // the user has visited
-                        map.put(Browser.BookmarkColumns.TITLE, title);
-                        map.put(Browser.BookmarkColumns.URL, url);
-                        map.put(Browser.BookmarkColumns.CREATED, creationTime);
-                        map.put(Browser.BookmarkColumns.BOOKMARK, 1);
-                        map.put(Browser.BookmarkColumns.DATE, 0);
-                        int visits = 0;
-                        if (count > 0) {
-                            // The user has already bookmarked, and possibly
-                            // visited this site.  However, they are creating
-                            // a new bookmark with the same url but a different
-                            // name.  The new bookmark should have the same
-                            // number of visits as the already created bookmark.
-                            visits = c.getInt(c.getColumnIndexOrThrow(
-                                    Browser.BookmarkColumns.VISITS));
-                        }
-                        // Bookmark starts with 3 extra visits so that it will
-                        // bubble up in the most visited and goto search box
-                        map.put(Browser.BookmarkColumns.VISITS, visits + 3);
-                        cr.insert(Browser.BOOKMARKS_URI, map);
-                    }
-                }
-                WebIconDatabase.getInstance().retainIconForPageUrl(url);
-                c.deactivate();
+                Bookmarks.addBookmark(null, getContentResolver(), url, title, true);
                 setResult(RESULT_OK);
             }
         } catch (IllegalStateException e) {
diff --git a/src/com/android/browser/AddNewBookmark.java b/src/com/android/browser/AddNewBookmark.java
index a75d002..5308f6b 100644
--- a/src/com/android/browser/AddNewBookmark.java
+++ b/src/com/android/browser/AddNewBookmark.java
@@ -47,17 +47,7 @@
         mUrlText = (TextView) findViewById(R.id.url);
         mImageView = (ImageView) findViewById(R.id.favicon);
     }
-    
-    /**
-     *  Copy this BookmarkItem to item.
-     *  @param item BookmarkItem to receive the info from this BookmarkItem.
-     */
-    /* package */ void copyTo(AddNewBookmark item) {
-        item.mTextView.setText(mTextView.getText());
-        item.mUrlText.setText(mUrlText.getText());
-        item.mImageView.setImageDrawable(mImageView.getDrawable());
-    }
-    
+
     /**
      *  Set the new url for the bookmark item.
      *  @param url  The new url for the bookmark item.
diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java
new file mode 100644
index 0000000..a3dc919
--- /dev/null
+++ b/src/com/android/browser/Bookmarks.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2009 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.Browser;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+import android.widget.Toast;
+
+import java.util.Date;
+
+/**
+ *  This class is purely to have a common place for adding/deleting bookmarks.
+ */
+/* package */ class Bookmarks {
+    private static final String     WHERE_CLAUSE
+            = "url = ? OR url = ? OR url = ? OR url = ?";
+    private static final String     WHERE_CLAUSE_SECURE = "url = ? OR url = ?";
+
+    private static String[]         SELECTION_ARGS;
+
+    /**
+     *  Add a bookmark to the database.
+     *  @param context Context of the calling Activity.  This is used to make
+     *          Toast confirming that the bookmark has been added.  If the
+     *          caller provides null, the Toast will not be shown.
+     *  @param cr The ContentResolver being used to add the bookmark to the db.
+     *  @param url URL of the website to be bookmarked.
+     *  @param name Provided name for the bookmark.
+     *  @param retainIcon Whether to retain the page's icon in the icon database.
+     *          This will usually be <code>true</code> except when bookmarks are
+     *          added by a settings restore agent.
+     */
+    /* package */ static void addBookmark(Context context,
+            ContentResolver cr, String url, String name,
+            boolean retainIcon) {
+        // Want to append to the beginning of the list
+        long creationTime = new Date().getTime();
+        // First we check to see if the user has already visited this
+        // site.  They may have bookmarked it in a different way from
+        // how it's stored in the database, so allow different combos
+        // to map to the same url.
+        boolean secure = false;
+        String compareString = url;
+        if (compareString.startsWith("http://")) {
+            compareString = compareString.substring(7);
+        } else if (compareString.startsWith("https://")) {
+            compareString = compareString.substring(8);
+            secure = true;
+        }
+        if (compareString.startsWith("www.")) {
+            compareString = compareString.substring(4);
+        }
+        if (secure) {
+            SELECTION_ARGS = new String[2];
+            SELECTION_ARGS[0] = "https://" + compareString;
+            SELECTION_ARGS[1] = "https://www." + compareString;
+        } else {
+            SELECTION_ARGS = new String[4];
+            SELECTION_ARGS[0] = compareString;
+            SELECTION_ARGS[1] = "www." + compareString;
+            SELECTION_ARGS[2] = "http://" + compareString;
+            SELECTION_ARGS[3] = "http://" + SELECTION_ARGS[1];
+        }
+        Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+                Browser.HISTORY_PROJECTION,
+                secure ? WHERE_CLAUSE_SECURE : WHERE_CLAUSE,
+                SELECTION_ARGS,
+                null);
+        ContentValues map = new ContentValues();
+        if (cursor.moveToFirst() && cursor.getInt(
+                Browser.HISTORY_PROJECTION_BOOKMARK_INDEX) == 0) {
+            // This means we have been to this site but not bookmarked
+            // it, so convert the history item to a bookmark
+            map.put(Browser.BookmarkColumns.CREATED, creationTime);
+            map.put(Browser.BookmarkColumns.TITLE, name);
+            map.put(Browser.BookmarkColumns.BOOKMARK, 1);
+            cr.update(Browser.BOOKMARKS_URI, map,
+                    "_id = " + cursor.getInt(0), null);
+        } else {
+            int count = cursor.getCount();
+            boolean matchedTitle = false;
+            for (int i = 0; i < count; i++) {
+                // One or more bookmarks already exist for this site.
+                // Check the names of each
+                cursor.moveToPosition(i);
+                if (cursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX)
+                        .equals(name)) {
+                    // The old bookmark has the same name.
+                    // Update its creation time.
+                    map.put(Browser.BookmarkColumns.CREATED,
+                            creationTime);
+                    cr.update(Browser.BOOKMARKS_URI, map,
+                            "_id = " + cursor.getInt(0), null);
+                    matchedTitle = true;
+                    break;
+                }
+            }
+            if (!matchedTitle) {
+                // Adding a bookmark for a site the user has visited,
+                // or a new bookmark (with a different name) for a site
+                // the user has visited
+                map.put(Browser.BookmarkColumns.TITLE, name);
+                map.put(Browser.BookmarkColumns.URL, url);
+                map.put(Browser.BookmarkColumns.CREATED, creationTime);
+                map.put(Browser.BookmarkColumns.BOOKMARK, 1);
+                map.put(Browser.BookmarkColumns.DATE, 0);
+                int visits = 0;
+                if (count > 0) {
+                    // The user has already bookmarked, and possibly
+                    // visited this site.  However, they are creating
+                    // a new bookmark with the same url but a different
+                    // name.  The new bookmark should have the same
+                    // number of visits as the already created bookmark.
+                    visits = cursor.getInt(
+                            Browser.HISTORY_PROJECTION_VISITS_INDEX);
+                }
+                // Bookmark starts with 3 extra visits so that it will
+                // bubble up in the most visited and goto search box
+                map.put(Browser.BookmarkColumns.VISITS, visits + 3);
+                cr.insert(Browser.BOOKMARKS_URI, map);
+            }
+        }
+        if (retainIcon) {
+            WebIconDatabase.getInstance().retainIconForPageUrl(url);
+        }
+        cursor.deactivate();
+        if (context != null) {
+            Toast.makeText(context, R.string.added_to_bookmarks,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     *  Remove a bookmark from the database.  If the url is a visited site, it
+     *  will remain in the database, but only as a history item, and not as a
+     *  bookmarked site.
+     *  @param context Context of the calling Activity.  This is used to make
+     *          Toast confirming that the bookmark has been removed.  If the
+     *          caller provides null, the Toast will not be shown.
+     *  @param cr The ContentResolver being used to remove the bookmark.
+     *  @param url URL of the website to be removed.
+     */
+    /* package */ static void removeFromBookmarks(Context context,
+            ContentResolver cr, String url) {
+        Cursor cursor = cr.query(
+                Browser.BOOKMARKS_URI,
+                Browser.HISTORY_PROJECTION,
+                "url = ?",
+                new String[] { url },
+                null);
+        boolean first = cursor.moveToFirst();
+        // Should be in the database no matter what
+        if (!first) {
+            throw new AssertionError("URL is not in the database!");
+        }
+        // Remove from bookmarks
+        WebIconDatabase.getInstance().releaseIconForPageUrl(url);
+        Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
+                cursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
+        int numVisits = cursor.getInt(
+                Browser.HISTORY_PROJECTION_VISITS_INDEX);
+        if (0 == numVisits) {
+            cr.delete(uri, null, null);
+        } else {
+            // It is no longer a bookmark, but it is still a visited
+            // site.
+            ContentValues values = new ContentValues();
+            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
+            try {
+                cr.update(uri, values, null, null);
+            } catch (IllegalStateException e) {
+                Log.e("removeFromBookmarks", "no database!");
+            }
+        }
+        if (context != null) {
+            Toast.makeText(context, R.string.removed_from_bookmarks,
+                    Toast.LENGTH_LONG).show();
+        }
+        cursor.deactivate();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index 93ba1b1..5134cf8 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -28,6 +28,7 @@
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.ContentResolver;
+import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -35,6 +36,7 @@
 import android.content.IntentFilter;
 import android.content.ServiceConnection;
 import android.content.DialogInterface.OnCancelListener;
+import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.AssetManager;
@@ -107,11 +109,14 @@
 import android.webkit.CookieSyncManager;
 import android.webkit.DownloadListener;
 import android.webkit.HttpAuthHandler;
+import android.webkit.PluginManager;
 import android.webkit.SslErrorHandler;
 import android.webkit.URLUtil;
 import android.webkit.WebChromeClient;
+import android.webkit.WebChromeClient.CustomViewCallback;
 import android.webkit.WebHistoryItem;
 import android.webkit.WebIconDatabase;
+import android.webkit.WebStorage;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 import android.widget.EditText;
@@ -121,6 +126,7 @@
 import android.widget.Toast;
 
 import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -583,11 +589,6 @@
                 }
 
                 copyBuildInfos();
-
-                // Refresh the plugin list.
-                if (mTabControl.getCurrentWebView() != null) {
-                    mTabControl.getCurrentWebView().refreshPlugins(false);
-                }
             } catch (IOException e) {
                 Log.e(TAG, "IO Exception: " + e);
             }
@@ -634,16 +635,22 @@
         }
     }
 
+    // Flag to enable the touchable browser bar with buttons
+    private final boolean CUSTOM_BROWSER_BAR = true;
+
     @Override public void onCreate(Bundle icicle) {
         if (LOGV_ENABLED) {
             Log.v(LOGTAG, this + " onStart");
         }
         super.onCreate(icicle);
-        this.requestWindowFeature(Window.FEATURE_LEFT_ICON);
-        this.requestWindowFeature(Window.FEATURE_RIGHT_ICON);
-        this.requestWindowFeature(Window.FEATURE_PROGRESS);
-        this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
-
+        if (CUSTOM_BROWSER_BAR) {
+            this.requestWindowFeature(Window.FEATURE_NO_TITLE);
+        } else {
+            this.requestWindowFeature(Window.FEATURE_LEFT_ICON);
+            this.requestWindowFeature(Window.FEATURE_RIGHT_ICON);
+            this.requestWindowFeature(Window.FEATURE_PROGRESS);
+            this.requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+        }
         // test the browser in OpenGL
         // requestWindowFeature(Window.FEATURE_OPENGL);
 
@@ -668,8 +675,37 @@
         mGenericFavicon = getResources().getDrawable(
                 R.drawable.app_web_browser_sm);
 
-        mContentView = (FrameLayout) getWindow().getDecorView().findViewById(
-                com.android.internal.R.id.content);
+        FrameLayout frameLayout = (FrameLayout) getWindow().getDecorView()
+                .findViewById(com.android.internal.R.id.content);
+        if (CUSTOM_BROWSER_BAR) {
+            // This FrameLayout will hold the custom FrameLayout and a LinearLayout
+            // that contains the title bar and a FrameLayout, which
+            // holds everything else.
+            FrameLayout browserFrameLayout = (FrameLayout) LayoutInflater.from(this)
+                    .inflate(R.layout.custom_screen, null);
+            mTitleBar = (TitleBar) browserFrameLayout.findViewById(R.id.title_bar);
+            mTitleBar.setBrowserActivity(this);
+            mContentView = (FrameLayout) browserFrameLayout.findViewById(
+                    R.id.main_content);
+            mErrorConsoleContainer = (LinearLayout) browserFrameLayout.findViewById(
+                    R.id.error_console);
+            mCustomViewContainer = (FrameLayout) browserFrameLayout
+                    .findViewById(R.id.fullscreen_custom_content);
+            frameLayout.addView(browserFrameLayout, COVER_SCREEN_PARAMS);
+        } else {
+            mCustomViewContainer = new FrameLayout(this);
+            mCustomViewContainer.setBackgroundColor(Color.BLACK);
+            mContentView = new FrameLayout(this);
+
+            LinearLayout linearLayout = new LinearLayout(this);
+            linearLayout.setOrientation(LinearLayout.VERTICAL);
+            mErrorConsoleContainer = new LinearLayout(this);
+            linearLayout.addView(mErrorConsoleContainer, new LinearLayout.LayoutParams(
+                    ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+            linearLayout.addView(mContentView, COVER_SCREEN_PARAMS);
+            frameLayout.addView(mCustomViewContainer, COVER_SCREEN_PARAMS);
+            frameLayout.addView(linearLayout, COVER_SCREEN_PARAMS);
+        }
 
         // Create the tab control and our initial tab
         mTabControl = new TabControl(this);
@@ -702,6 +738,52 @@
                 }
             };
 
+        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        filter.addDataScheme("package");
+        mPackageInstallationReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                final String action = intent.getAction();
+                final String packageName = intent.getData()
+                        .getSchemeSpecificPart();
+                final boolean replacing = intent.getBooleanExtra(
+                        Intent.EXTRA_REPLACING, false);
+                if (Intent.ACTION_PACKAGE_REMOVED.equals(action) && replacing) {
+                    // if it is replacing, refreshPlugins() when adding
+                    return;
+                }
+                PackageManager pm = BrowserActivity.this.getPackageManager();
+                PackageInfo pkgInfo = null;
+                try {
+                    pkgInfo = pm.getPackageInfo(packageName,
+                            PackageManager.GET_PERMISSIONS);
+                } catch (PackageManager.NameNotFoundException e) {
+                    return;
+                }
+                if (pkgInfo != null) {
+                    String permissions[] = pkgInfo.requestedPermissions;
+                    if (permissions == null) {
+                        return;
+                    }
+                    boolean permissionOk = false;
+                    for (String permit : permissions) {
+                        if (PluginManager.PLUGIN_PERMISSION.equals(permit)) {
+                            permissionOk = true;
+                            break;
+                        }
+                    }
+                    if (permissionOk) {
+                        PluginManager.getInstance(BrowserActivity.this)
+                                .refreshPlugins(
+                                        Intent.ACTION_PACKAGE_ADDED
+                                                .equals(action));
+                    }
+                }
+            }
+        };
+        registerReceiver(mPackageInstallationReceiver, filter);
+
         // If this was a web search request, pass it on to the default web search provider.
         if (handleWebSearchIntent(getIntent())) {
             moveTaskToBack(true);
@@ -713,6 +795,9 @@
             // none of the files in the directory are referenced any more.
             new ClearThumbnails().execute(
                     mTabControl.getThumbnailDir().listFiles());
+            // there is no quit on Android. But if we can't restore the state,
+            // we can treat it as a new Browser, remove the old session cookies.
+            CookieManager.getInstance().removeSessionCookie();
             final Intent intent = getIntent();
             final Bundle extra = intent.getExtras();
             // Create an initial tab.
@@ -765,6 +850,12 @@
             // are not animating from the tab picker.
             attachTabToContentView(mTabControl.getCurrentTab());
         }
+
+        // Read JavaScript flags if it exists.
+        String jsFlags = mSettings.getJsFlags();
+        if (jsFlags.trim().length() != 0) {
+            mTabControl.getCurrentWebView().setJsFlags(jsFlags);
+        }
     }
 
     @Override
@@ -812,7 +903,7 @@
             if (Intent.ACTION_VIEW.equals(action)
                     && !getPackageName().equals(appId)
                     && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
-                final TabControl.Tab appTab = mTabControl.getTabFromId(appId);
+                TabControl.Tab appTab = mTabControl.getTabFromId(appId);
                 if (appTab != null) {
                     Log.i(LOGTAG, "Reusing tab for " + appId);
                     // Dismiss the subwindow if applicable.
@@ -825,14 +916,14 @@
                     // page, it can be reused.
                     boolean needsLoad =
                             mTabControl.recreateWebView(appTab, urlData.mUrl);
-                    
+
                     if (current != appTab) {
                         showTab(appTab, needsLoad ? urlData : EMPTY_URL_DATA);
                     } else {
                         if (mTabOverview != null && mAnimationCount == 0) {
                             sendAnimateFromOverview(appTab, false,
-                                    needsLoad ? urlData : EMPTY_URL_DATA, TAB_OVERVIEW_DELAY,
-                                    null);
+                                    needsLoad ? urlData : EMPTY_URL_DATA,
+                                    TAB_OVERVIEW_DELAY, null);
                         } else {
                             // If the tab was the current tab, we have to attach
                             // it to the view system again.
@@ -843,12 +934,32 @@
                         }
                     }
                     return;
+                } else {
+                    // No matching application tab, try to find a regular tab
+                    // with a matching url.
+                    appTab = mTabControl.findUnusedTabWithUrl(urlData.mUrl);
+                    if (appTab != null) {
+                        if (current != appTab) {
+                            // Use EMPTY_URL_DATA so we do not reload the page
+                            showTab(appTab, EMPTY_URL_DATA);
+                        } else {
+                            if (mTabOverview != null && mAnimationCount == 0) {
+                                sendAnimateFromOverview(appTab, false,
+                                        EMPTY_URL_DATA, TAB_OVERVIEW_DELAY,
+                                        null);
+                            }
+                            // Don't do anything here since we are on the
+                            // correct page.
+                        }
+                    } else {
+                        // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url
+                        // will be opened in a new tab unless we have reached
+                        // MAX_TABS. Then the url will be opened in the current
+                        // tab. If a new tab is created, it will have "true" for
+                        // exit on close.
+                        openTabAndShow(urlData, null, true, appId);
+                    }
                 }
-                // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url will be
-                // opened in a new tab unless we have reached MAX_TABS. Then the
-                // url will be opened in the current tab. If a new tab is
-                // created, it will have "true" for exit on close.
-                openTabAndShow(urlData, null, true, appId);
             } else {
                 if ("about:debug".equals(urlData.mUrl)) {
                     mSettings.toggleDebugSettings();
@@ -1107,8 +1218,9 @@
             return;
         }
 
+        mTabControl.resumeCurrentTab();
         mActivityInPause = false;
-        resumeWebView();
+        resumeWebViewTimers();
 
         if (mWakeLock.isHeld()) {
             mHandler.removeMessages(RELEASE_WAKELOCK);
@@ -1166,8 +1278,9 @@
             return;
         }
 
+        mTabControl.pauseCurrentTab();
         mActivityInPause = true;
-        if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) {
+        if (mTabControl.getCurrentIndex() >= 0 && !pauseWebViewTimers()) {
             mWakeLock.acquire();
             mHandler.sendMessageDelayed(mHandler
                     .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
@@ -1218,6 +1331,8 @@
         //        "com.android.masfproxyservice",
         //        "com.android.masfproxyservice.MasfProxyService"));
         //stopService(proxyServiceIntent);
+
+        unregisterReceiver(mPackageInstallationReceiver);
     }
 
     @Override
@@ -1266,7 +1381,7 @@
         mTabControl.freeMemory();
     }
 
-    private boolean resumeWebView() {
+    private boolean resumeWebViewTimers() {
         if ((!mActivityInPause && !mPageStarted) ||
                 (mActivityInPause && mPageStarted)) {
             CookieSyncManager.getInstance().startSync();
@@ -1280,7 +1395,7 @@
         }
     }
 
-    private boolean pauseWebView() {
+    private boolean pauseWebViewTimers() {
         if (mActivityInPause && !mPageStarted) {
             CookieSyncManager.getInstance().stopSync();
             WebView w = mTabControl.getCurrentWebView();
@@ -1441,7 +1556,7 @@
      */
     @Override
     public boolean onSearchRequested() {
-        String url = getTopWindow().getUrl();
+        String url = (getTopWindow() == null) ? null : getTopWindow().getUrl();
         startSearch(mSettings.getHomePage().equals(url) ? null : url, true,
                 createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), false);
         return true;
@@ -1877,6 +1992,20 @@
         final WebView main = t.getWebView();
         // Attach the main WebView.
         mContentView.addView(main, COVER_SCREEN_PARAMS);
+
+        if (mShouldShowErrorConsole) {
+            ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true);
+            if (errorConsole.numberOfErrors() == 0) {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+            } else {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
+            }
+
+            mErrorConsoleContainer.addView(errorConsole,
+                    new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+                                                  ViewGroup.LayoutParams.WRAP_CONTENT));
+        }
+
         // Attach the sub window if necessary
         attachSubWindow(t);
         // Request focus on the top window.
@@ -1898,6 +2027,11 @@
     private void removeTabFromContentView(TabControl.Tab t) {
         // Remove the main WebView.
         mContentView.removeView(t.getWebView());
+
+        if (mTabControl.getCurrentErrorConsole(false) != null) {
+            mErrorConsoleContainer.removeView(mTabControl.getCurrentErrorConsole(false));
+        }
+
         // Remove the sub window if it exists.
         if (t.getSubWebView() != null) {
             mContentView.removeView(t.getSubWebViewContainer());
@@ -2377,7 +2511,11 @@
         // While the tab overview is animating or being shown, block changes
         // to the title.
         if (mAnimationCount == 0 && mTabOverview == null) {
-            setTitle(buildUrlTitle(url, title));
+            if (CUSTOM_BROWSER_BAR) {
+                mTitleBar.setTitleAndUrl(title, url);
+            } else {
+                setTitle(buildUrlTitle(url, title));
+            }
         }
     }
 
@@ -2418,7 +2556,7 @@
      * or an empty string if, for example, the URL in question is a
      * file:// URL with no hostname.
      */
-    private static String buildTitleUrl(String url) {
+    /* package */ static String buildTitleUrl(String url) {
         String titleUrl = null;
 
         if (url != null) {
@@ -2454,18 +2592,34 @@
         if (mAnimationCount > 0 || mTabOverview != null) {
             return;
         }
-        Drawable[] array = new Drawable[2];
-        PaintDrawable p = new PaintDrawable(Color.WHITE);
-        p.setCornerRadius(3f);
-        array[0] = p;
-        if (icon == null) {
-            array[1] = mGenericFavicon;
+        if (CUSTOM_BROWSER_BAR) {
+            Drawable[] array = new Drawable[3];
+            array[0] = new PaintDrawable(Color.BLACK);
+            PaintDrawable p = new PaintDrawable(Color.WHITE);
+            array[1] = p;
+            if (icon == null) {
+                array[2] = mGenericFavicon;
+            } else {
+                array[2] = new BitmapDrawable(icon);
+            }
+            LayerDrawable d = new LayerDrawable(array);
+            d.setLayerInset(1, 1, 1, 1, 1);
+            d.setLayerInset(2, 2, 2, 2, 2);
+            mTitleBar.setFavicon(d);
         } else {
-            array[1] = new BitmapDrawable(icon);
+            Drawable[] array = new Drawable[2];
+            PaintDrawable p = new PaintDrawable(Color.WHITE);
+            p.setCornerRadius(3f);
+            array[0] = p;
+            if (icon == null) {
+                array[1] = mGenericFavicon;
+            } else {
+                array[1] = new BitmapDrawable(icon);
+            }
+            LayerDrawable d = new LayerDrawable(array);
+            d.setLayerInset(1, 2, 2, 2, 2);
+            getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d);
         }
-        LayerDrawable d = new LayerDrawable(array);
-        d.setLayerInset(1, 2, 2, 2, 2);
-        getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, d);
     }
 
     /**
@@ -2543,9 +2697,9 @@
                         finish();
                         return;
                     }
-                    // call pauseWebView() now, we won't be able to call it in
-                    // onPause() as the WebView won't be valid.
-                    pauseWebView();
+                    // call pauseWebViewTimers() now, we won't be able to call
+                    // it in onPause() as the WebView won't be valid.
+                    pauseWebViewTimers();
                     removeTabFromContentView(current);
                     mTabControl.removeTab(current);
                 }
@@ -2569,10 +2723,15 @@
         // because of accumulated key events,
         // we should ignore it as browser is not active any more.
         WebView topWindow = getTopWindow();
-        if (topWindow == null)
+        if (topWindow == null && mCustomView == null)
             return KeyTracker.State.NOT_TRACKING;
 
         if (keyCode == KeyEvent.KEYCODE_BACK) {
+            // Check if a custom view is currently showing and, if it is, hide it.
+            if (mCustomView != null) {
+                mWebChromeClient.onHideCustomView();
+                return KeyTracker.State.DONE_TRACKING;
+            }
             // During animations, block the back key so that other animations
             // are not triggered and so that we don't end up destroying all the
             // WebViews before finishing the animation.
@@ -2771,6 +2930,58 @@
         }
     };
 
+    private void updateScreenshot(WebView view) {
+        // If this is a bookmarked site, add a screenshot to the database.
+        // FIXME: When should we update?  Every time?
+        // FIXME: Would like to make sure there is actually something to
+        // draw, but the API for that (WebViewCore.pictureReady()) is not
+        // currently accessible here.
+        String original = view.getOriginalUrl();
+        if (original != null) {
+            // copied from BrowserBookmarksAdapter
+            int query = original.indexOf('?');
+            String noQuery = original;
+            if (query != -1) {
+                noQuery = original.substring(0, query);
+            }
+            String URL = noQuery + '?';
+            String[] selArgs = new String[] { noQuery, URL };
+            final String where
+                    = "(url == ? OR url GLOB ? || '*') AND bookmark == 1";
+            final String[] projection
+                    = new String[] { Browser.BookmarkColumns._ID };
+            ContentResolver cr = getContentResolver();
+            final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection,
+                    where, selArgs, null);
+            boolean succeed = c.moveToFirst();
+            ContentValues values = null;
+            while (succeed) {
+                if (values == null) {
+                    final ByteArrayOutputStream os
+                            = new ByteArrayOutputStream();
+                    Picture thumbnail = view.capturePicture();
+                    // Keep width and height in sync with BrowserBookmarksPage
+                    // and bookmark_thumb
+                    Bitmap bm = Bitmap.createBitmap(100, 80,
+                            Bitmap.Config.ARGB_4444);
+                    Canvas canvas = new Canvas(bm);
+                    // May need to tweak these values to determine what is the
+                    // best scale factor
+                    canvas.scale(.5f, .5f);
+                    thumbnail.draw(canvas);
+                    bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                    values = new ContentValues();
+                    values.put(Browser.BookmarkColumns.THUMBNAIL,
+                            os.toByteArray());
+                }
+                cr.update(ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
+                        c.getInt(0)), values, null, null);
+                succeed = c.moveToNext();
+            }
+            c.close();
+        }
+    }
+
     // -------------------------------------------------------------------------
     // WebViewClient implementation.
     //-------------------------------------------------------------------------
@@ -2798,6 +3009,15 @@
         public void onPageStarted(WebView view, String url, Bitmap favicon) {
             resetLockIcon(url);
             setUrlTitle(url, null);
+
+            ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(false);
+            if (errorConsole != null) {
+                errorConsole.clearErrorMessages();
+                if (mShouldShowErrorConsole) {
+                    errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+                }
+            }
+
             // Call updateIcon instead of setFavicon so the bookmark
             // database can be updated.
             updateIcon(url, favicon);
@@ -2846,8 +3066,9 @@
 
             if (!mPageStarted) {
                 mPageStarted = true;
-                // if onResume() has been called, resumeWebView() does nothing.
-                resumeWebView();
+                // if onResume() has been called, resumeWebViewTimers() does
+                // nothing.
+                resumeWebViewTimers();
             }
 
             // reset sync timer to avoid sync starts during loading a page
@@ -2881,6 +3102,7 @@
 
             // Update the lock icon image only once we are done loading
             updateLockIconImage(mLockIconType);
+            updateScreenshot(view);
 
             // Performance probe
             if (false) {
@@ -2968,9 +3190,9 @@
 
             if (mPageStarted) {
                 mPageStarted = false;
-                // pauseWebView() will do nothing and return false if onPause()
-                // is not called yet.
-                if (pauseWebView()) {
+                // pauseWebViewTimers() will do nothing and return false if
+                // onPause() is not called yet.
+                if (pauseWebViewTimers()) {
                     if (mWakeLock.isHeld()) {
                         mHandler.removeMessages(RELEASE_WAKELOCK);
                         mWakeLock.release();
@@ -3016,9 +3238,9 @@
             if (url.startsWith("about:")) {
                 return false;
             }
-            
+
             Intent intent;
-            
+
             // perform generic parsing of the URI to turn it into an Intent.
             try {
                 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
@@ -3516,8 +3738,13 @@
             // Block progress updates to the title bar while the tab overview
             // is animating or being displayed.
             if (mAnimationCount == 0 && mTabOverview == null) {
-                getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
-                        newProgress * 100);
+                if (CUSTOM_BROWSER_BAR) {
+                    mTitleBar.setProgress(newProgress);
+                } else {
+                    getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+                            newProgress * 100);
+
+                }
             }
 
             if (newProgress == 100) {
@@ -3551,6 +3778,8 @@
                 url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
                 return;
             }
+            // See if we can find the current url in our history database and
+            // add the new title to it.
             if (url.startsWith("http://www.")) {
                 url = url.substring(11);
             } else if (url.startsWith("http://")) {
@@ -3565,9 +3794,6 @@
                 Cursor c = mResolver.query(Browser.BOOKMARKS_URI,
                     Browser.HISTORY_PROJECTION, where, selArgs, null);
                 if (c.moveToFirst()) {
-                    if (LOGV_ENABLED) {
-                        Log.v(LOGTAG, "updating cursor");
-                    }
                     // Current implementation of database only has one entry per
                     // url.
                     ContentValues map = new ContentValues();
@@ -3587,6 +3813,96 @@
         public void onReceivedIcon(WebView view, Bitmap icon) {
             updateIcon(view.getUrl(), icon);
         }
+
+        @Override
+        public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
+            if (mCustomView != null)
+                return;
+
+            // Add the custom view to its container.
+            mCustomViewContainer.addView(view, COVER_SCREEN_GRAVITY_CENTER);
+            mCustomView = view;
+            mCustomViewCallback = callback;
+            // Save the menu state and set it to empty while the custom
+            // view is showing.
+            mOldMenuState = mMenuState;
+            mMenuState = EMPTY_MENU;
+            // Hide the content view.
+            mContentView.setVisibility(View.GONE);
+            // Finally show the custom view container.
+            mCustomViewContainer.setVisibility(View.VISIBLE);
+            mCustomViewContainer.bringToFront();
+        }
+
+        @Override
+        public void onHideCustomView() {
+            if (mCustomView == null)
+                return;
+
+            // Hide the custom view.
+            mCustomView.setVisibility(View.GONE);
+            // Remove the custom view from its container.
+            mCustomViewContainer.removeView(mCustomView);
+            mCustomView = null;
+            // Reset the old menu state.
+            mMenuState = mOldMenuState;
+            mOldMenuState = EMPTY_MENU;
+            mCustomViewContainer.setVisibility(View.GONE);
+            mCustomViewCallback.onCustomViewHidden();
+            // Show the content view.
+            mContentView.setVisibility(View.VISIBLE);
+        }
+
+        /**
+         * The origin has exceeded its database quota.
+         * @param url the URL that exceeded the quota
+         * @param databaseIdentifier the identifier of the database on
+         *     which the transaction that caused the quota overflow was run
+         * @param currentQuota the current quota for the origin.
+         * @param totalUsedQuota is the sum of all origins' quota.
+         * @param quotaUpdater The callback to run when a decision to allow or
+         *     deny quota has been made. Don't forget to call this!
+         */
+        @Override
+        public void onExceededDatabaseQuota(String url,
+            String databaseIdentifier, long currentQuota, long totalUsedQuota,
+            WebStorage.QuotaUpdater quotaUpdater) {
+            mSettings.getWebStorageSizeManager().onExceededDatabaseQuota(
+                    url, databaseIdentifier, currentQuota, totalUsedQuota,
+                    quotaUpdater);
+        }
+
+        /**
+         * The Application Cache has exceeded its max size.
+         * @param spaceNeeded is the amount of disk space that would be needed
+         * in order for the last appcache operation to succeed.
+         * @param totalUsedQuota is the sum of all origins' quota.
+         * @param quotaUpdater A callback to inform the WebCore thread that a new
+         * app cache size is available. This callback must always be executed at
+         * some point to ensure that the sleeping WebCore thread is woken up.
+         */
+        @Override
+        public void onReachedMaxAppCacheSize(long spaceNeeded,
+                long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+            mSettings.getWebStorageSizeManager().onReachedMaxAppCacheSize(
+                    spaceNeeded, totalUsedQuota, quotaUpdater);
+        }
+
+        /* Adds a JavaScript error message to the system log.
+         * @param message The error message to report.
+         * @param lineNumber The line number of the error.
+         * @param sourceID The name of the source file that caused the error.
+         */
+        @Override
+        public void addMessageToConsole(String message, int lineNumber, String sourceID) {
+            ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true);
+            errorConsole.addErrorMessage(message, sourceID, lineNumber);
+                if (mShouldShowErrorConsole &&
+                        errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
+                    errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
+                }
+            Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":" + lineNumber);
+        }
     };
 
     /**
@@ -3702,19 +4018,19 @@
         String cookies = CookieManager.getInstance().getCookie(url);
 
         ContentValues values = new ContentValues();
-        values.put(Downloads.URI, uri.toString());
-        values.put(Downloads.COOKIE_DATA, cookies);
-        values.put(Downloads.USER_AGENT, userAgent);
-        values.put(Downloads.NOTIFICATION_PACKAGE,
+        values.put(Downloads.COLUMN_URI, uri.toString());
+        values.put(Downloads.COLUMN_COOKIE_DATA, cookies);
+        values.put(Downloads.COLUMN_USER_AGENT, userAgent);
+        values.put(Downloads.COLUMN_NOTIFICATION_PACKAGE,
                 getPackageName());
-        values.put(Downloads.NOTIFICATION_CLASS,
+        values.put(Downloads.COLUMN_NOTIFICATION_CLASS,
                 BrowserDownloadPage.class.getCanonicalName());
-        values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
-        values.put(Downloads.MIMETYPE, mimetype);
-        values.put(Downloads.FILENAME_HINT, filename);
-        values.put(Downloads.DESCRIPTION, uri.getHost());
+        values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+        values.put(Downloads.COLUMN_MIME_TYPE, mimetype);
+        values.put(Downloads.COLUMN_FILE_NAME_HINT, filename);
+        values.put(Downloads.COLUMN_DESCRIPTION, uri.getHost());
         if (contentLength > 0) {
-            values.put(Downloads.TOTAL_BYTES, contentLength);
+            values.put(Downloads.COLUMN_TOTAL_BYTES, contentLength);
         }
         if (mimetype == null) {
             // We must have long pressed on a link or image to download it. We
@@ -3780,7 +4096,11 @@
         // If the tab overview is animating or being shown, do not update the
         // lock icon.
         if (mAnimationCount == 0 && mTabOverview == null) {
-            getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
+            if (CUSTOM_BROWSER_BAR) {
+                mTitleBar.setLock(d);
+            } else {
+                getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
+            }
         }
     }
 
@@ -4273,7 +4593,17 @@
                     String data = intent.getAction();
                     Bundle extras = intent.getExtras();
                     if (extras != null && extras.getBoolean("new_window", false)) {
-                        openTab(data);
+                        final TabControl.Tab newTab = openTab(data);
+                        if (mSettings.openInBackground() &&
+                                newTab != null && mTabOverview != null) {
+                            mTabControl.populatePickerData(newTab);
+                            mTabControl.setCurrentTab(newTab);
+                            mTabOverview.add(newTab);
+                            mTabOverview.setCurrentIndex(
+                                    mTabControl.getCurrentIndex());
+                            sendAnimateFromOverview(newTab, false,
+                                    EMPTY_URL_DATA, TAB_OVERVIEW_DELAY, null);
+                        }
                     } else {
                         final TabControl.Tab currentTab =
                                 mTabControl.getCurrentTab();
@@ -4281,8 +4611,8 @@
                         // middle of an animation, animate away from it to the
                         // current tab.
                         if (mTabOverview != null && mAnimationCount == 0) {
-                            sendAnimateFromOverview(currentTab, false, new UrlData(data),
-                                    TAB_OVERVIEW_DELAY, null);
+                            sendAnimateFromOverview(currentTab, false,
+                                    new UrlData(data), TAB_OVERVIEW_DELAY, null);
                         } else {
                             dismissSubWindow(currentTab);
                             if (data != null && data.length() != 0) {
@@ -4332,8 +4662,8 @@
                 // was clicked on.
                 if (mTabControl.getTabCount() == 0) {
                     current = mTabControl.createNewTab();
-                    sendAnimateFromOverview(current, true,
-                            new UrlData(mSettings.getHomePage()), TAB_OVERVIEW_DELAY, null);
+                    sendAnimateFromOverview(current, true, new UrlData(
+                            mSettings.getHomePage()), TAB_OVERVIEW_DELAY, null);
                 } else {
                     final int index = position > 0 ? (position - 1) : 0;
                     current = mTabControl.getTab(index);
@@ -4369,8 +4699,8 @@
             if (index == ImageGrid.NEW_TAB) {
                 openTabAndShow(mSettings.getHomePage(), null, false, null);
             } else {
-                sendAnimateFromOverview(mTabControl.getTab(index),
-                        false, EMPTY_URL_DATA, 0, null);
+                sendAnimateFromOverview(mTabControl.getTab(index), false,
+                        EMPTY_URL_DATA, 0, null);
             }
         }
     }
@@ -4392,13 +4722,19 @@
         AnimatingView(Context ctxt, TabControl.Tab t) {
             super(ctxt);
             mTab = t;
-            // Use the top window in the animation since the tab overview will
-            // display the top window in each cell.
-            final WebView w = t.getTopWindow();
-            mPicture = w.capturePicture();
-            mScale = w.getScale() / w.getWidth();
-            mScrollX = w.getScrollX();
-            mScrollY = w.getScrollY();
+            if (t != null && t.getTopWindow() != null) {
+                // Use the top window in the animation since the tab overview
+                // will display the top window in each cell.
+                final WebView w = t.getTopWindow();
+                mPicture = w.capturePicture();
+                mScale = w.getScale() / w.getWidth();
+                mScrollX = w.getScrollX();
+                mScrollY = w.getScrollY();
+            } else {
+                mPicture = null;
+                mScale = 1.0f;
+                mScrollX = mScrollY = 0;
+            }
         }
 
         @Override
@@ -4472,16 +4808,20 @@
         mAnimationCount++;
         // Always change the title bar to the window overview title while
         // animating.
-        getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
-        getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null);
-        getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
-                Window.PROGRESS_VISIBILITY_OFF);
-        setTitle(R.string.tab_picker_title);
+        if (CUSTOM_BROWSER_BAR) {
+            mTitleBar.setToTabPicker();
+        } else {
+            getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
+            getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null);
+            getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+                    Window.PROGRESS_VISIBILITY_OFF);
+            setTitle(R.string.tab_picker_title);
+        }
         // Make the menu empty until the animation completes.
         mMenuState = EMPTY_MENU;
     }
 
-    private void bookmarksOrHistoryPicker(boolean startWithHistory) {
+    /* package */ void bookmarksOrHistoryPicker(boolean startWithHistory) {
         WebView current = mTabControl.getCurrentWebView();
         if (current == null) {
             return;
@@ -4557,7 +4897,7 @@
       return 0;
     }
 
-    static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
+    protected static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
             "(?i)" + // switch on case insensitive matching
             "(" +    // begin group for schema
             "(?:http|https|file):\\/\\/" +
@@ -4622,6 +4962,34 @@
         return URLUtil.composeSearchUrl(inUrl, QuickSearch_G, QUERY_PLACE_HOLDER);
     }
 
+    /* package */ void setShouldShowErrorConsole(boolean flag) {
+        if (flag == mShouldShowErrorConsole) {
+            // Nothing to do.
+            return;
+        }
+
+        mShouldShowErrorConsole = flag;
+
+        ErrorConsoleView errorConsole = mTabControl.getCurrentErrorConsole(true);
+
+        if (flag) {
+            // Setting the show state of the console will cause it's the layout to be inflated.
+            if (errorConsole.numberOfErrors() > 0) {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
+            } else {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+            }
+
+            // Now we can add it to the main view.
+            mErrorConsoleContainer.addView(errorConsole,
+                    new LinearLayout.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
+                                                  ViewGroup.LayoutParams.WRAP_CONTENT));
+        } else {
+            mErrorConsoleContainer.removeView(errorConsole);
+        }
+
+    }
+
     private final static int LOCK_ICON_UNSECURE = 0;
     private final static int LOCK_ICON_SECURE   = 1;
     private final static int LOCK_ICON_MIXED    = 2;
@@ -4634,11 +5002,15 @@
     private ContentResolver mResolver;
     private FrameLayout     mContentView;
     private ImageGrid       mTabOverview;
+    private View            mCustomView;
+    private FrameLayout     mCustomViewContainer;
+    private WebChromeClient.CustomViewCallback mCustomViewCallback;
 
     // FIXME, temp address onPrepareMenu performance problem. When we move everything out of
     // view, we should rewrite this.
     private int mCurrentMenuState = 0;
     private int mMenuState = R.id.MAIN_MENU;
+    private int mOldMenuState = EMPTY_MENU;
     private static final int EMPTY_MENU = -1;
     private Menu mMenu;
 
@@ -4729,6 +5101,11 @@
                                             new FrameLayout.LayoutParams(
                                             ViewGroup.LayoutParams.FILL_PARENT,
                                             ViewGroup.LayoutParams.FILL_PARENT);
+    /*package*/ static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
+                                            new FrameLayout.LayoutParams(
+                                            ViewGroup.LayoutParams.FILL_PARENT,
+                                            ViewGroup.LayoutParams.FILL_PARENT,
+                                            Gravity.CENTER);
     // Google search
     final static String QuickSearch_G = "http://www.google.com/m?q=%s";
     // Wikipedia search
@@ -4762,6 +5139,11 @@
 
     private Toast mStopToast;
 
+    private TitleBar mTitleBar;
+
+    private LinearLayout mErrorConsoleContainer = null;
+    private boolean mShouldShowErrorConsole = false;
+
     // Used during animations to prevent other animations from being triggered.
     // A count is used since the animation to and from the Window overview can
     // overlap. A count of 0 means no animation where a count of > 0 means
@@ -4779,10 +5161,12 @@
     private IntentFilter mNetworkStateChangedFilter;
     private BroadcastReceiver mNetworkStateIntentReceiver;
 
+    private BroadcastReceiver mPackageInstallationReceiver;
+
     // activity requestCode
-    final static int COMBO_PAGE             = 1;
-    final static int DOWNLOAD_PAGE          = 2;
-    final static int PREFERENCES_PAGE       = 3;
+    final static int COMBO_PAGE                 = 1;
+    final static int DOWNLOAD_PAGE              = 2;
+    final static int PREFERENCES_PAGE           = 3;
 
     // the frenquency of checking whether system memory is low
     final static int CHECK_MEMORY_INTERVAL = 30000;     // 30 seconds
@@ -4832,7 +5216,7 @@
         String mEncoding;
         @Override
         boolean isEmpty() {
-            return mInlined == null || mInlined.length() == 0 || super.isEmpty(); 
+            return mInlined == null || mInlined.length() == 0 || super.isEmpty();
         }
 
         @Override
diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java
new file mode 100644
index 0000000..c239b12
--- /dev/null
+++ b/src/com/android/browser/BrowserBackupAgent.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2009 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.IOException;
+
+import android.app.BackupAgent;
+import android.backup.BackupDataInput;
+import android.backup.BackupDataOutput;
+import android.database.Cursor;
+import android.os.ParcelFileDescriptor;
+import android.provider.Browser;
+import android.provider.Browser.BookmarkColumns;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.zip.CRC32;
+
+/**
+ * Settings backup agent for the Android browser.  Currently the only thing
+ * stored is the set of bookmarks.  It's okay if I/O exceptions are thrown
+ * out of the agent; the calling code handles it and the backup operation
+ * simply fails.
+ *
+ * @hide
+ */
+public class BrowserBackupAgent extends BackupAgent {
+    static final String TAG = "BrowserBookmarkAgent";
+    static final boolean DEBUG = true;
+
+    static final String BOOKMARK_KEY = "_bookmarks_";
+    /** this version num MUST be incremented if the flattened-file schema ever changes */
+    static final int BACKUP_AGENT_VERSION = 0;
+
+    /**
+     * In order to determine whether the bookmark set has changed since the
+     * last time we did a backup, we store the following bits of info in the
+     * state file after a backup:
+     *
+     * 1. the size of the flattened bookmark file
+     * 2. the CRC32 of that file
+     * 3. the agent version number [relevant following an OTA]
+     *
+     * After we flatten the bookmarks file here in onBackup, we compare its
+     * metrics with the values from the saved state.  If they match, it means
+     * the bookmarks didn't really change and we don't need to send the data.
+     * (If they don't match, of course, then they've changed and we do indeed
+     * send the new flattened file to be backed up.)
+     */
+    @Override
+    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+            ParcelFileDescriptor newState) throws IOException {
+        long savedFileSize = -1;
+        long savedCrc = -1;
+        int savedVersion = -1;
+
+        // Extract the previous bookmark file size & CRC from the saved state
+        DataInputStream in = new DataInputStream(
+                new FileInputStream(oldState.getFileDescriptor()));
+        try {
+            savedFileSize = in.readLong();
+            savedCrc = in.readLong();
+            savedVersion = in.readInt();
+        } catch (EOFException e) {
+            // It means we had no previous state; that's fine
+        }
+
+        // Build a flattened representation of the bookmarks table
+        File tmpfile = File.createTempFile("bkp", null, getCacheDir());
+        try {
+            FileOutputStream outfstream = new FileOutputStream(tmpfile);
+            long newCrc = buildBookmarkFile(outfstream);
+            outfstream.close();
+
+            // Any changes since the last backup?
+            if ((savedVersion != BACKUP_AGENT_VERSION)
+                    || (newCrc != savedCrc)
+                    || (tmpfile.length() != savedFileSize)) {
+                // Different checksum or different size, so we need to back it up
+                copyFileToBackup(BOOKMARK_KEY, tmpfile, data);
+            }
+
+            // Record our backup state and we're done
+            writeBackupState(tmpfile.length(), newCrc, newState);
+        } finally {
+            // Make sure to tidy up when we're done
+            tmpfile.delete();
+        }
+    }
+
+    /**
+     * Restore from backup -- reads in the flattened bookmark file as supplied from
+     * the backup service, parses that out, and rebuilds the bookmarks table in the
+     * browser database from it.
+     */
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode,
+            ParcelFileDescriptor newState) throws IOException {
+        long crc = -1;
+        File tmpfile = File.createTempFile("rst", null, getFilesDir());
+        try {
+            while (data.readNextHeader()) {
+                if (BOOKMARK_KEY.equals(data.getKey())) {
+                    // Read the flattened bookmark data into a temp file
+                    crc = copyBackupToFile(data, tmpfile, data.getDataSize());
+
+                    FileInputStream infstream = new FileInputStream(tmpfile);
+                    DataInputStream in = new DataInputStream(infstream);
+
+                    try {
+                        int count = in.readInt();
+                        ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count);
+
+                        // Read all the bookmarks, then process later -- if we can't read
+                        // all the data successfully, we don't touch the bookmarks table
+                        for (int i = 0; i < count; i++) {
+                            Bookmark mark = new Bookmark();
+                            mark.url = in.readUTF();
+                            mark.visits = in.readInt();
+                            mark.date = in.readLong();
+                            mark.created = in.readLong();
+                            mark.title = in.readUTF();
+                            bookmarks.add(mark);
+                        }
+
+                        // Okay, we have all the bookmarks -- now see if we need to add
+                        // them to the browser's database
+                        int N = bookmarks.size();
+                        if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks");
+                        String[] urlCol = new String[] { BookmarkColumns.URL };
+                        for (int i = 0; i < N; i++) {
+                            Bookmark mark = bookmarks.get(i);
+
+                            // Does this URL exist in the bookmark table?
+                            Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
+                                    urlCol,  BookmarkColumns.URL + " == '" + mark.url + "' AND " +
+                                    BookmarkColumns.BOOKMARK + " == 1 ", null, null);
+                            // if not, insert it
+                            if (cursor.getCount() <= 0) {
+                                Log.v(TAG, "Did not see url: " + mark.url);
+                                // Right now we do not reconstruct the db entry in its
+                                // entirety; we just add a new bookmark with the same data
+                                Bookmarks.addBookmark(null, getContentResolver(),
+                                        mark.url, mark.title, false);
+                            } else {
+                                Log.v(TAG, "Skipping extant url: " + mark.url);
+                            }
+                            cursor.close();
+                        }
+                    } catch (IOException ioe) {
+                        Log.w(TAG, "Bad backup data; not restoring");
+                        crc = -1;
+                    }
+                }
+
+                // Last, write the state we just restored from so we can discern
+                // changes whenever we get invoked for backup in the future
+                writeBackupState(tmpfile.length(), crc, newState);
+            }
+        } finally {
+            // Whatever happens, delete the temp file
+            tmpfile.delete();
+        }
+    }
+
+    class Bookmark {
+        public String url;
+        public int visits;
+        public long date;
+        public long created;
+        public String title;
+    }
+    /*
+     * Utility functions
+     */
+
+    // Flatten the bookmarks table into the given file, calculating its CRC in the process
+    private long buildBookmarkFile(FileOutputStream outfstream) throws IOException {
+        CRC32 crc = new CRC32();
+        ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512);
+        DataOutputStream bout = new DataOutputStream(bufstream);
+
+        Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
+                new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS,
+                BookmarkColumns.DATE, BookmarkColumns.CREATED,
+                BookmarkColumns.TITLE },
+                BookmarkColumns.BOOKMARK + " == 1 ", null, null);
+
+        // The first thing in the file is the row count...
+        int count = cursor.getCount();
+        if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks");
+        bout.writeInt(count);
+        byte[] record = bufstream.toByteArray();
+        crc.update(record);
+        outfstream.write(record);
+
+        // ... followed by the data for each row
+        for (int i = 0; i < count; i++) {
+            cursor.moveToNext();
+
+            String url = cursor.getString(0);
+            int visits = cursor.getInt(1);
+            long date = cursor.getLong(2);
+            long created = cursor.getLong(3);
+            String title = cursor.getString(4);
+
+            // construct the flattened record in a byte array
+            bufstream.reset();
+            bout.writeUTF(url);
+            bout.writeInt(visits);
+            bout.writeLong(date);
+            bout.writeLong(created);
+            bout.writeUTF(title);
+
+            // Update the CRC and write the record to the temp file
+            record = bufstream.toByteArray();
+            crc.update(record);
+            outfstream.write(record);
+
+            if (DEBUG) Log.v(TAG, "   wrote url " + url);
+        }
+
+        cursor.close();
+        return crc.getValue();
+    }
+
+    // Write the file to backup as a single record under the given key
+    private void copyFileToBackup(String key, File file, BackupDataOutput data)
+            throws IOException {
+        final int CHUNK = 8192;
+        byte[] buf = new byte[CHUNK];
+
+        int toCopy = (int) file.length();
+        data.writeEntityHeader(key, toCopy);
+
+        FileInputStream in = new FileInputStream(file);
+        int nRead;
+        while (toCopy > 0) {
+            nRead = in.read(buf, 0, CHUNK);
+            data.writeEntityData(buf, nRead);
+            toCopy -= nRead;
+        }
+        in.close();
+    }
+
+    // Read the given file from backup to a file, calculating a CRC32 along the way
+    private long copyBackupToFile(BackupDataInput data, File file, int toRead)
+            throws IOException {
+        final int CHUNK = 8192;
+        byte[] buf = new byte[CHUNK];
+        CRC32 crc = new CRC32();
+        FileOutputStream out = new FileOutputStream(file);
+
+        while (toRead > 0) {
+            int numRead = data.readEntityData(buf, 0, CHUNK);
+            crc.update(buf, 0, numRead);
+            out.write(buf, 0, numRead);
+            toRead -= numRead;
+        }
+
+        out.close();
+        return crc.getValue();
+    }
+
+    // Write the given metrics to the new state file
+    private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)
+            throws IOException {
+        DataOutputStream out = new DataOutputStream(
+                new FileOutputStream(stateFile.getFileDescriptor()));
+        out.writeLong(fileSize);
+        out.writeLong(crc);
+        out.writeInt(BACKUP_AGENT_VERSION);
+    }
+}
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
index 27782e0..75be45b 100644
--- a/src/com/android/browser/BrowserBookmarksAdapter.java
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -30,29 +30,27 @@
 import android.provider.Browser;
 import android.provider.Browser.BookmarkColumns;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.webkit.WebIconDatabase;
 import android.webkit.WebIconDatabase.IconListener;
 import android.widget.BaseAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
 
 import java.io.ByteArrayOutputStream;
 
 class BrowserBookmarksAdapter extends BaseAdapter {
 
-    private final String            LOGTAG = "Bookmarks";
-
     private String                  mCurrentPage;
+    private String                  mCurrentTitle;
     private Cursor                  mCursor;
     private int                     mCount;
-    private String                  mLastWhereClause;
-    private String[]                mLastSelectionArgs;
-    private String                  mLastOrderBy;
     private BrowserBookmarksPage    mBookmarksPage;
     private ContentResolver         mContentResolver;
-    private ChangeObserver          mChangeObserver;
-    private DataSetObserver         mDataSetObserver;
     private boolean                 mDataValid;
+    private boolean                 mGridMode;
 
     // When true, this adapter is used to pick a bookmark to create a shortcut
     private boolean mCreateShortcut;
@@ -70,36 +68,37 @@
 
     /**
      *  Create a new BrowserBookmarksAdapter.
-     *  @param b        BrowserBookmarksPage that instantiated this.  
-     *                  Necessary so it will adjust its focus
-     *                  appropriately after a search.
-     */
-    public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage) {
-        this(b, curPage, false);
-    }
-
-    /**
-     *  Create a new BrowserBookmarksAdapter.
      *  @param b        BrowserBookmarksPage that instantiated this.
      *                  Necessary so it will adjust its focus
      *                  appropriately after a search.
      */
     public BrowserBookmarksAdapter(BrowserBookmarksPage b, String curPage,
-            boolean createShortcut) {
+            String curTitle, boolean createShortcut) {
         mDataValid = false;
         mCreateShortcut = createShortcut;
         mExtraOffset = createShortcut ? 0 : 1;
         mBookmarksPage = b;
-        mCurrentPage = b.getResources().getString(R.string.current_page) + 
-                curPage;
+        mCurrentPage = b.getResources().getString(R.string.current_page)
+                + curPage;
+        mCurrentTitle = curTitle;
         mContentResolver = b.getContentResolver();
-        mLastOrderBy = Browser.BookmarkColumns.CREATED + " DESC";
-        mChangeObserver = new ChangeObserver();
-        mDataSetObserver = new MyDataSetObserver();
+        mGridMode = false;
+
         // FIXME: Should have a default sort order that the user selects.
-        search(null);
+        String whereClause = Browser.BookmarkColumns.BOOKMARK + " != 0";
+        String orderBy = Browser.BookmarkColumns.VISITS + " DESC";
+        mCursor = b.managedQuery(Browser.BOOKMARKS_URI,
+                Browser.HISTORY_PROJECTION, whereClause, null, orderBy);
+        mCursor.registerContentObserver(new ChangeObserver());
+        mCursor.registerDataSetObserver(new MyDataSetObserver());
+
+        mDataValid = true;
+        notifyDataSetChanged();
+
+        mCount = mCursor.getCount() + mExtraOffset;
+
         // FIXME: This requires another query of the database after the
-        // initial search(null). Can we optimize this?
+        // managedQuery. Can we optimize this?
         Browser.requestAllIcons(mContentResolver,
                 Browser.BookmarkColumns.FAVICON + " is NULL AND " +
                 Browser.BookmarkColumns.BOOKMARK + " == 1", mIconReceiver);
@@ -181,18 +180,7 @@
         }
         mCursor.moveToPosition(position- mExtraOffset);
         String url = mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX);
-        WebIconDatabase.getInstance().releaseIconForPageUrl(url);
-        Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI, mCursor
-                .getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
-        int numVisits = mCursor.getInt(Browser.HISTORY_PROJECTION_VISITS_INDEX);
-        if (0 == numVisits) {
-            mContentResolver.delete(uri, null, null);
-        } else {
-            // It is no longer a bookmark, but it is still a visited site.
-            ContentValues values = new ContentValues();
-            values.put(Browser.BookmarkColumns.BOOKMARK, 0);
-            mContentResolver.update(uri, values, null, null);
-        }
+        Bookmarks.removeFromBookmarks(null, mContentResolver, url);
         refreshList();
     }
     
@@ -253,52 +241,9 @@
      *  Refresh list to recognize a change in the database.
      */
     public void refreshList() {
-        // FIXME: consider using requery().
-        // Need to do more work to get it to function though.
-        searchInternal(mLastWhereClause, mLastSelectionArgs, mLastOrderBy);
-    }
-
-    /**
-     *  Search the database for bookmarks that match the input string.
-     *  @param like String to use to search the database.  Strings with spaces 
-     *              are treated as having multiple search terms using the
-     *              OR operator.  Search both the title and url.
-     */
-    public void search(String like) {
-        String whereClause = Browser.BookmarkColumns.BOOKMARK + " == 1";
-        String[] selectionArgs = null;
-        if (like != null) {
-            String[] likes = like.split(" ");
-            int count = 0;
-            boolean firstTerm = true;
-            StringBuilder andClause = new StringBuilder(256);
-            for (int j = 0; j < likes.length; j++) {
-                if (likes[j].length() > 0) {
-                    if (firstTerm) {
-                        firstTerm = false;
-                    } else {
-                        andClause.append(" OR ");
-                    }
-                    andClause.append(Browser.BookmarkColumns.TITLE
-                            + " LIKE ? OR " + Browser.BookmarkColumns.URL
-                            + " LIKE ? ");
-                    count += 2;
-                }
-            }
-            if (count > 0) {
-                selectionArgs = new String[count];
-                count = 0;
-                for (int j = 0; j < likes.length; j++) {
-                    if (likes[j].length() > 0) {
-                        like = "%" + likes[j] + "%";
-                        selectionArgs[count++] = like;
-                        selectionArgs[count++] = like;
-                    }
-                }
-                whereClause += " AND (" + andClause + ")";
-            }
-        }
-        searchInternal(whereClause, selectionArgs, mLastOrderBy);
+        mCursor.requery();
+        mCount = mCursor.getCount() + mExtraOffset;
+        notifyDataSetChanged();
     }
 
     /**
@@ -348,46 +293,6 @@
     }
 
     /**
-     *  This sorts alphabetically, with non-capitalized titles before 
-     *  capitalized.
-     */
-    public void sortAlphabetical() {
-        searchInternal(mLastWhereClause, mLastSelectionArgs,
-                Browser.BookmarkColumns.TITLE + " COLLATE UNICODE ASC");
-    }
-
-    /**
-     *  Internal function used in search, sort, and refreshList.
-     */
-    private void searchInternal(String whereClause, String[] selectionArgs,
-            String orderBy) {
-        if (mCursor != null) {
-            mCursor.unregisterContentObserver(mChangeObserver);
-            mCursor.unregisterDataSetObserver(mDataSetObserver);
-            mBookmarksPage.stopManagingCursor(mCursor);
-            mCursor.deactivate();
-        }
-
-        mLastWhereClause = whereClause;
-        mLastSelectionArgs = selectionArgs;
-        mLastOrderBy = orderBy;
-        mCursor = mContentResolver.query(
-            Browser.BOOKMARKS_URI,
-            Browser.HISTORY_PROJECTION,
-            whereClause,
-            selectionArgs, 
-            orderBy);
-        mCursor.registerContentObserver(mChangeObserver);
-        mCursor.registerDataSetObserver(mDataSetObserver);
-        mBookmarksPage.startManagingCursor(mCursor);
-
-        mDataValid = true;
-        notifyDataSetChanged();
-
-        mCount = mCursor.getCount() + mExtraOffset;
-    }
-
-    /**
      * How many items should be displayed in the list.
      * @return Count of items.
      */
@@ -425,6 +330,20 @@
         return position;
     }
 
+    /* package */ void switchViewMode(boolean toGrid) {
+        mGridMode = toGrid;
+    }
+
+    /* package */ void populateBookmarkItem(BookmarkItem b, int position) {
+        mCursor.moveToPosition(position - mExtraOffset);
+        b.setUrl(mCursor.getString(Browser.HISTORY_PROJECTION_URL_INDEX));
+        b.setName(mCursor.getString(Browser.HISTORY_PROJECTION_TITLE_INDEX));
+        byte[] data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
+        Bitmap bitmap = (null == data) ? null :
+                BitmapFactory.decodeByteArray(data, 0, data.length);
+        b.setFavicon(bitmap);
+    }
+
     /**
      * Get a View that displays the data at the specified position
      * in the list.
@@ -440,6 +359,53 @@
             throw new AssertionError(
                     "BrowserBookmarksAdapter tried to get a view out of range");
         }
+        if (mGridMode) {
+            if (convertView == null || convertView instanceof AddNewBookmark
+                    || convertView instanceof BookmarkItem) {
+                LayoutInflater factory = LayoutInflater.from(mBookmarksPage);
+                convertView
+                        = factory.inflate(R.layout.bookmark_thumbnail, null);
+            }
+            View holder = convertView.findViewById(R.id.holder);
+            ImageView thumb = (ImageView) convertView.findViewById(R.id.thumb);
+            ImageView fav = (ImageView) convertView.findViewById(R.id.fav);
+            TextView tv = (TextView) convertView.findViewById(R.id.label);
+
+            if (0 == position && !mCreateShortcut) {
+                // This is to create a bookmark for the current page.
+                holder.setVisibility(View.VISIBLE);
+                fav.setVisibility(View.GONE);
+                tv.setText(mCurrentTitle);
+                // FIXME: Want to show the screenshot of the current page
+                thumb.setImageResource(R.drawable.blank);
+                return convertView;
+            }
+            holder.setVisibility(View.GONE);
+            mCursor.moveToPosition(position - mExtraOffset);
+            tv.setText(mCursor.getString(
+                    Browser.HISTORY_PROJECTION_TITLE_INDEX));
+            byte[] data = mCursor.getBlob(
+                    Browser.HISTORY_PROJECTION_THUMBNAIL_INDEX);
+            if (data == null) {
+                // Backup is to just show white
+                thumb.setImageResource(R.drawable.blank);
+            } else {
+                thumb.setImageBitmap(
+                        BitmapFactory.decodeByteArray(data, 0, data.length));
+            }
+            // Now show the favicon
+            data = mCursor.getBlob(Browser.HISTORY_PROJECTION_FAVICON_INDEX);
+            if (data == null) {
+                fav.setVisibility(View.GONE);
+            } else {
+                fav.setVisibility(View.VISIBLE);
+                fav.setImageBitmap(
+                        BitmapFactory.decodeByteArray(data, 0, data.length));
+            }
+
+            return convertView;
+
+        }
         if (position == 0 && !mCreateShortcut) {
             AddNewBookmark b;
             if (convertView instanceof AddNewBookmark) {
@@ -450,7 +416,7 @@
             b.setUrl(mCurrentPage);
             return b;
         }
-        if (convertView == null || convertView instanceof AddNewBookmark) {
+        if (convertView == null || !(convertView instanceof BookmarkItem)) {
             convertView = new BookmarkItem(mBookmarksPage);
         }
         bind((BookmarkItem)convertView, position);
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
index dd34c14..428aa92 100644
--- a/src/com/android/browser/BrowserBookmarksPage.java
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -36,6 +36,7 @@
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -43,7 +44,9 @@
 import android.view.ViewGroup;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.widget.AdapterView;
+import android.widget.GridView;
 import android.widget.ListView;
+import android.widget.Toast;
 
 /**
  *  View showing the user's bookmarks in the browser.
@@ -51,6 +54,9 @@
 public class BrowserBookmarksPage extends Activity implements 
         View.OnCreateContextMenuListener {
 
+    private boolean                 mGridMode;
+    private GridView                mGridPage;
+    private View                    mVerticalList;
     private BrowserBookmarksAdapter mBookmarksAdapter;
     private static final int        BOOKMARKS_SAVE = 1;
     private boolean                 mMaxTabsOpen;
@@ -107,7 +113,13 @@
             break;
         case R.id.copy_url_context_menu_id:
             copy(getUrl(i.position));
-            
+            break;
+        case R.id.homepage_context_menu_id:
+            BrowserSettings.getInstance().setHomePage(this,
+                    getUrl(i.position));
+            Toast.makeText(this, R.string.homepage_set,
+                    Toast.LENGTH_LONG).show();
+            break;
         default:
             return super.onContextItemSelected(item);
         }
@@ -131,25 +143,29 @@
                     ((ViewGroup) mAddHeader.getParent()).
                             removeView(mAddHeader);
                 }
-                ((AddNewBookmark) i.targetView).copyTo(mAddHeader);
+                mAddHeader.setUrl(getIntent().getStringExtra("url"));
                 menu.setHeaderView(mAddHeader);
                 return;
             }
             menu.setGroupVisible(R.id.ADD_MENU, false);
-            BookmarkItem b = (BookmarkItem) i.targetView;
+            if (mMaxTabsOpen) {
+                menu.findItem(R.id.new_window_context_menu_id).setVisible(
+                        false);
+            }
             if (mContextHeader == null) {
                 mContextHeader = new BookmarkItem(BrowserBookmarksPage.this);
             } else if (mContextHeader.getParent() != null) {
                 ((ViewGroup) mContextHeader.getParent()).
                         removeView(mContextHeader);
             }
-            b.copyTo(mContextHeader);
-            menu.setHeaderView(mContextHeader);
-
-            if (mMaxTabsOpen) {
-                menu.findItem(R.id.new_window_context_menu_id).setVisible(
-                        false);
+            if (mGridMode) {
+                mBookmarksAdapter.populateBookmarkItem(mContextHeader,
+                        i.position);
+            } else {
+                BookmarkItem b = (BookmarkItem) i.targetView;
+                b.copyTo(mContextHeader);
             }
+            menu.setHeaderView(mContextHeader);
         }
 
     /**
@@ -159,25 +175,61 @@
     protected void onCreate(Bundle icicle) {
         super.onCreate(icicle);
 
-        setContentView(R.layout.browser_bookmarks_page);
-        setTitle(R.string.browser_bookmarks_page_bookmarks_text);
-
         if (Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction())) {
             mCreateShortcut = true;
         }
-
-        mBookmarksAdapter = new BrowserBookmarksAdapter(this, 
-                getIntent().getStringExtra("url"), mCreateShortcut);
         mMaxTabsOpen = getIntent().getBooleanExtra("maxTabsOpen", false);
 
-        ListView listView = (ListView) findViewById(R.id.list);
-        listView.setAdapter(mBookmarksAdapter);
-        listView.setDrawSelectorOnTop(false);
-        listView.setVerticalScrollBarEnabled(true);
-        listView.setOnItemClickListener(mListener);
+        setTitle(R.string.browser_bookmarks_page_bookmarks_text);
+        mBookmarksAdapter = new BrowserBookmarksAdapter(this,
+                        getIntent().getStringExtra("url"),
+                        getIntent().getStringExtra("title"), mCreateShortcut);
+        switchViewMode(true);
+    }
 
-        if (!mCreateShortcut) {
-            listView.setOnCreateContextMenuListener(this);
+    /**
+     *  Set the ContentView to be either the grid of thumbnails or the vertical
+     *  list.  Pass true to set it to the grid.
+     */
+    private void switchViewMode(boolean gridMode) {
+        mGridMode = gridMode;
+        mBookmarksAdapter.switchViewMode(gridMode);
+        if (mGridMode) {
+            if (mGridPage == null) {
+                mGridPage = new GridView(this);
+                mGridPage.setAdapter(mBookmarksAdapter);
+                mGridPage.setOnItemClickListener(mListener);
+                mGridPage.setNumColumns(GridView.AUTO_FIT);
+                // Keep this in sync with bookmark_thumb and
+                // BrowserActivity.updateScreenshot
+                mGridPage.setColumnWidth(100);
+                mGridPage.setFocusable(true);
+                mGridPage.setFocusableInTouchMode(true);
+                mGridPage.setSelector(android.R.drawable.gallery_thumb);
+                mGridPage.setVerticalSpacing(10);
+                if (!mCreateShortcut) {
+                    mGridPage.setOnCreateContextMenuListener(this);
+                }
+            }
+            setContentView(mGridPage);
+        } else {
+            if (null == mVerticalList) {
+                LayoutInflater factory = LayoutInflater.from(this);
+                mVerticalList = factory.inflate(R.layout.browser_bookmarks_page,
+                        null);
+
+                ListView listView
+                        = (ListView) mVerticalList.findViewById(R.id.list);
+                listView.setAdapter(mBookmarksAdapter);
+                listView.setDrawSelectorOnTop(false);
+                listView.setVerticalScrollBarEnabled(true);
+                listView.setOnItemClickListener(mListener);
+
+                if (!mCreateShortcut) {
+                    listView.setOnCreateContextMenuListener(this);
+                }
+            }
+            setContentView(mVerticalList);
         }
     }
 
@@ -196,7 +248,7 @@
             // It is possible that the view has been canceled when we get to
             // this point as back has a higher priority 
             if (mCanceled) {
-                android.util.Log.e("browser", "item clicked when dismising");
+                android.util.Log.e(LOGTAG, "item clicked when dismissing");
                 return;
             }
             if (!mCreateShortcut) {
@@ -294,12 +346,16 @@
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
-            case R.id.new_context_menu_id:
-                saveCurrentPage();
-                break;
-                
-            default:
-                return super.onOptionsItemSelected(item);
+        case R.id.new_context_menu_id:
+            saveCurrentPage();
+            break;
+
+        case R.id.switch_mode_menu_id:
+            switchViewMode(!mGridMode);
+            break;
+
+        default:
+            return super.onOptionsItemSelected(item);
         }
         return true;
     }
@@ -370,7 +426,7 @@
     /**
      *  Refresh the shown list after the database has changed.
      */
-    public void refreshList() {
+    private void refreshList() {
         mBookmarksAdapter.refreshList();
     }
     
diff --git a/src/com/android/browser/BrowserDownloadAdapter.java b/src/com/android/browser/BrowserDownloadAdapter.java
index 38b83fe..16cb982 100644
--- a/src/com/android/browser/BrowserDownloadAdapter.java
+++ b/src/com/android/browser/BrowserDownloadAdapter.java
@@ -60,14 +60,14 @@
     public BrowserDownloadAdapter(Context context, int layout, Cursor c) {
         super(context, layout, c);
         mFilenameColumnId = c.getColumnIndexOrThrow(Downloads._DATA);
-        mTitleColumnId = c.getColumnIndexOrThrow(Downloads.TITLE);
-        mDescColumnId = c.getColumnIndexOrThrow(Downloads.DESCRIPTION);
-        mStatusColumnId = c.getColumnIndexOrThrow(Downloads.STATUS);
-        mTotalBytesColumnId = c.getColumnIndexOrThrow(Downloads.TOTAL_BYTES);
+        mTitleColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_TITLE);
+        mDescColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_DESCRIPTION);
+        mStatusColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_STATUS);
+        mTotalBytesColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_TOTAL_BYTES);
         mCurrentBytesColumnId = 
-            c.getColumnIndexOrThrow(Downloads.CURRENT_BYTES);
-        mMimetypeColumnId = c.getColumnIndexOrThrow(Downloads.MIMETYPE);
-        mDateColumnId = c.getColumnIndexOrThrow(Downloads.LAST_MODIFICATION);
+            c.getColumnIndexOrThrow(Downloads.COLUMN_CURRENT_BYTES);
+        mMimetypeColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_MIME_TYPE);
+        mDateColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_LAST_MODIFICATION);
     }
 
     @Override
@@ -106,7 +106,7 @@
                 // We have a filename, so we can build a title from that
                 title = new File(fullFilename).getName();
                 ContentValues values = new ContentValues();
-                values.put(Downloads.TITLE, title);
+                values.put(Downloads.COLUMN_TITLE, title);
                 // assume "_id" is the first column for the cursor 
                 context.getContentResolver().update(
                         ContentUris.withAppendedId(Downloads.CONTENT_URI,
diff --git a/src/com/android/browser/BrowserDownloadPage.java b/src/com/android/browser/BrowserDownloadPage.java
index 4397337..22e0e65 100644
--- a/src/com/android/browser/BrowserDownloadPage.java
+++ b/src/com/android/browser/BrowserDownloadPage.java
@@ -66,34 +66,30 @@
         setTitle(getText(R.string.download_title));
 
         mListView = (ListView) findViewById(R.id.list);
-        LayoutInflater factory = LayoutInflater.from(this);
-        View v = factory.inflate(R.layout.no_downloads, null);
-        addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
-                LayoutParams.FILL_PARENT));
-        mListView.setEmptyView(v);
+        mListView.setEmptyView(findViewById(R.id.empty));
         
         mDownloadCursor = managedQuery(Downloads.CONTENT_URI, 
-                new String [] {"_id", Downloads.TITLE, Downloads.STATUS,
-                Downloads.TOTAL_BYTES, Downloads.CURRENT_BYTES, 
-                Downloads._DATA, Downloads.DESCRIPTION, 
-                Downloads.MIMETYPE, Downloads.LAST_MODIFICATION,
-                Downloads.VISIBILITY}, 
+                new String [] {"_id", Downloads.COLUMN_TITLE, Downloads.COLUMN_STATUS,
+                Downloads.COLUMN_TOTAL_BYTES, Downloads.COLUMN_CURRENT_BYTES, 
+                Downloads._DATA, Downloads.COLUMN_DESCRIPTION, 
+                Downloads.COLUMN_MIME_TYPE, Downloads.COLUMN_LAST_MODIFICATION,
+                Downloads.COLUMN_VISIBILITY}, 
                 null, null);
         
         // only attach everything to the listbox if we can access
         // the download database. Otherwise, just show it empty
         if (mDownloadCursor != null) {
             mStatusColumnId = 
-                    mDownloadCursor.getColumnIndexOrThrow(Downloads.STATUS);
+                    mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_STATUS);
             mIdColumnId =
                     mDownloadCursor.getColumnIndexOrThrow(Downloads._ID);
             mTitleColumnId = 
-                    mDownloadCursor.getColumnIndexOrThrow(Downloads.TITLE);
+                    mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_TITLE);
             
             // Create a list "controller" for the data
             mDownloadAdapter = new BrowserDownloadAdapter(this, 
                     R.layout.browser_download_item, mDownloadCursor);
-            
+
             mListView.setAdapter(mDownloadAdapter);
             mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
             mListView.setOnCreateContextMenuListener(this);
@@ -403,7 +399,7 @@
                 mDownloadCursor.getColumnIndexOrThrow(Downloads._DATA);
         String filename = mDownloadCursor.getString(filenameColumnId);
         int mimetypeColumnId =
-                mDownloadCursor.getColumnIndexOrThrow(Downloads.MIMETYPE);
+                mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_MIME_TYPE);
         String mimetype = mDownloadCursor.getString(mimetypeColumnId);
         Uri path = Uri.parse(filename);
         // If there is no scheme, then it must be a file
@@ -453,13 +449,13 @@
     private void hideCompletedDownload() {
         int status = mDownloadCursor.getInt(mStatusColumnId);
 
-        int visibilityColumn = mDownloadCursor.getColumnIndexOrThrow(Downloads.VISIBILITY);
+        int visibilityColumn = mDownloadCursor.getColumnIndexOrThrow(Downloads.COLUMN_VISIBILITY);
         int visibility = mDownloadCursor.getInt(visibilityColumn);
 
         if (Downloads.isStatusCompleted(status) &&
                 visibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
             ContentValues values = new ContentValues();
-            values.put(Downloads.VISIBILITY, Downloads.VISIBILITY_VISIBLE);
+            values.put(Downloads.COLUMN_VISIBILITY, Downloads.VISIBILITY_VISIBLE);
             getContentResolver().update(
                     ContentUris.withAppendedId(Downloads.CONTENT_URI,
                     mDownloadCursor.getLong(mIdColumnId)), values, null, null);
diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java
index 42ca848..335d8fe 100644
--- a/src/com/android/browser/BrowserHistoryPage.java
+++ b/src/com/android/browser/BrowserHistoryPage.java
@@ -41,6 +41,7 @@
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.view.ContextMenu.ContextMenuInfo;
+import android.view.ViewStub;
 import android.webkit.DateSorter;
 import android.webkit.WebIconDatabase.IconListener;
 import android.widget.AdapterView;
@@ -48,6 +49,7 @@
 import android.widget.ExpandableListView;
 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
 import android.widget.TextView;
+import android.widget.Toast;
 
 import java.util.List;
 import java.util.Vector;
@@ -60,6 +62,7 @@
     private HistoryAdapter          mAdapter;
     private DateSorter              mDateSorter;
     private boolean                 mMaxTabsOpen;
+    private HistoryItem             mContextHeader;
 
     private final static String LOGTAG = "browser";
 
@@ -110,8 +113,7 @@
         setListAdapter(mAdapter);
         final ExpandableListView list = getExpandableListView();
         list.setOnCreateContextMenuListener(this);
-        LayoutInflater factory = LayoutInflater.from(this);
-        View v = factory.inflate(R.layout.empty_history, null);
+        View v = new ViewStub(this, R.layout.empty_history);
         addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
                 LayoutParams.FILL_PARENT));
         list.setEmptyView(v);
@@ -165,7 +167,7 @@
         }  
         return super.onOptionsItemSelected(item);
     }
-    
+
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v,
             ContextMenuInfo menuInfo) {
@@ -180,12 +182,25 @@
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.historycontext, menu);
 
+        HistoryItem historyItem = (HistoryItem) i.targetView;
+
         // Setup the header
-        menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());
+        if (mContextHeader == null) {
+            mContextHeader = new HistoryItem(this);
+        } else if (mContextHeader.getParent() != null) {
+            ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
+        }
+        historyItem.copyTo(mContextHeader);
+        menu.setHeaderView(mContextHeader);
 
         // Only show open in new tab if we have not maxed out available tabs
         menu.findItem(R.id.new_window_context_menu_id).setVisible(!mMaxTabsOpen);
         
+        // For a bookmark, provide the option to remove it from bookmarks
+        if (historyItem.isBookmark()) {
+            MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id);
+            item.setTitle(R.string.remove_from_bookmarks);
+        }
         // decide whether to show the share link option
         PackageManager pm = getPackageManager();
         Intent send = new Intent(Intent.ACTION_SEND);
@@ -200,8 +215,9 @@
     public boolean onContextItemSelected(MenuItem item) {
         ExpandableListContextMenuInfo i = 
             (ExpandableListContextMenuInfo) item.getMenuInfo();
-        String url = ((HistoryItem)i.targetView).getUrl();
-        String title = ((HistoryItem)i.targetView).getName();
+        HistoryItem historyItem = (HistoryItem) i.targetView;
+        String url = historyItem.getUrl();
+        String title = historyItem.getName();
         switch (item.getItemId()) {
             case R.id.open_context_menu_id:
                 loadUrl(url, false);
@@ -210,7 +226,12 @@
                 loadUrl(url, true);
                 return true;
             case R.id.save_to_bookmarks_menu_id:
-                Browser.saveBookmark(this, title, url);
+                if (historyItem.isBookmark()) {
+                    Bookmarks.removeFromBookmarks(this, getContentResolver(),
+                            url);
+                } else {
+                    Browser.saveBookmark(this, title, url);
+                }
                 return true;
             case R.id.share_link_context_menu_id:
                 Browser.sendString(this, url);
@@ -222,6 +243,11 @@
                 Browser.deleteFromHistory(getContentResolver(), url);
                 mAdapter.refreshData();
                 return true;
+            case R.id.homepage_context_menu_id:
+                BrowserSettings.getInstance().setHomePage(this, url);
+                Toast.makeText(this, R.string.homepage_set,
+                    Toast.LENGTH_LONG).show();
+                return true;
             default:
                 break;
         }
@@ -267,18 +293,25 @@
         
         // Array for each of our bins.  Each entry represents how many items are
         // in that bin.
-        int mItemMap[];
+        private int mItemMap[];
         // This is our GroupCount.  We will have at most DateSorter.DAY_COUNT
         // bins, less if the user has no items in one or more bins.
-        int mNumberOfBins;
-        Vector<DataSetObserver> mObservers;
-        Cursor mCursor;
+        private int mNumberOfBins;
+        private Vector<DataSetObserver> mObservers;
+        private Cursor mCursor;
         
         HistoryAdapter() {
             mObservers = new Vector<DataSetObserver>();
             
-            String whereClause = Browser.BookmarkColumns.VISITS + " > 0 ";
-            String orderBy = Browser.BookmarkColumns.DATE + " DESC";
+            final String whereClause = Browser.BookmarkColumns.VISITS + " > 0"
+                    // In AddBookmarkPage, where we save new bookmarks, we add
+                    // three visits to newly created bookmarks, so that
+                    // bookmarks that have not been visited will show up in the
+                    // most visited, and higher in the goto search box.
+                    // However, this puts the site in the history, unless we
+                    // ignore sites with a DATE of 0, which the next line does.
+                    + " AND " + Browser.BookmarkColumns.DATE + " > 0";
+            final String orderBy = Browser.BookmarkColumns.DATE + " DESC";
            
             mCursor = managedQuery(
                     Browser.BOOKMARKS_URI,
@@ -290,6 +323,9 @@
         }
         
         void refreshData() {
+            if (mCursor.isClosed()) {
+                return;
+            }
             mCursor.requery();
             buildMap();
             for (DataSetObserver o : mObservers) {
diff --git a/src/com/android/browser/BrowserHomepagePreference.java b/src/com/android/browser/BrowserHomepagePreference.java
index d4708c3..7324f24 100644
--- a/src/com/android/browser/BrowserHomepagePreference.java
+++ b/src/com/android/browser/BrowserHomepagePreference.java
@@ -50,8 +50,8 @@
         if (dialog != null) {
             String url = s.toString();
             dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(
-                    url.length() == 0 || url.equals("about:blank") ||
-                    Regex.WEB_URL_PATTERN.matcher(url).matches());
+                url.length() == 0 || 
+                BrowserActivity.ACCEPTED_URI_SCHEMA.matcher(url).matches());
         }
     }
 
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
index 3b747d1..2348af0 100644
--- a/src/com/android/browser/BrowserPreferencesPage.java
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -17,19 +17,29 @@
 package com.android.browser;
 
 import java.util.List;
+import java.util.Vector;
+import java.util.Set;
 
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.preference.EditTextPreference;
+import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
-import android.webkit.WebView;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import android.webkit.GeolocationPermissions;
 import android.webkit.Plugin;
+import android.webkit.WebStorage;
+import android.webkit.WebView;
 
 public class BrowserPreferencesPage extends PreferenceActivity
         implements Preference.OnPreferenceChangeListener, 
         Preference.OnPreferenceClickListener {
 
+    String TAG = "BrowserPreferencesPage";
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -59,13 +69,36 @@
 
         e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING);
         e.setOnPreferenceChangeListener(this);
-        
+
         if (BrowserSettings.getInstance().showDebugSettings()) {
             addPreferencesFromResource(R.xml.debug_preferences);
         }
-        
+
         e = findPreference(BrowserSettings.PREF_GEARS_SETTINGS);
         e.setOnPreferenceClickListener(this);
+
+        PreferenceScreen websiteSettings = (PreferenceScreen)
+            findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS);
+        Intent intent = new Intent(this, WebsiteSettingsActivity.class);
+        websiteSettings.setIntent(intent);
+    }
+
+    /*
+     * We need to set the PreferenceScreen state in onResume(), as the number of
+     * origins with active features (WebStorage, Geolocation etc) could have
+     * changed after calling the WebsiteSettingsActivity.
+     */
+    @Override
+    protected void onResume() {
+        super.onResume();
+        PreferenceScreen websiteSettings = (PreferenceScreen)
+            findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS);
+        Set webStorageOrigins = WebStorage.getInstance().getOrigins();
+        Set geolocationOrigins =
+            GeolocationPermissions.getInstance().getOrigins();
+        websiteSettings.setEnabled(
+            ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) ||
+            ((geolocationOrigins != null) && !geolocationOrigins.isEmpty()));
     }
 
     @Override
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index a3ccf04..cdab3a3 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -19,8 +19,10 @@
 import com.google.android.providers.GoogleSettings.Partner;
 
 import android.app.SearchManager;
+import android.backup.BackupManager;
 import android.content.ComponentName;
 import android.content.ContentProvider;
+import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
@@ -40,6 +42,7 @@
 import android.preference.PreferenceManager;
 import android.provider.Browser;
 import android.provider.Settings;
+import android.provider.Browser.BookmarkColumns;
 import android.server.search.SearchableInfo;
 import android.text.TextUtils;
 import android.text.util.Regex;
@@ -54,6 +57,7 @@
 public class BrowserProvider extends ContentProvider {
 
     private SQLiteOpenHelper mOpenHelper;
+    private BackupManager mBackupManager;
     private static final String sDatabaseName = "browser.db";
     private static final String TAG = "BrowserProvider";
     private static final String ORDER_BY = "visits DESC, date DESC";
@@ -96,6 +100,8 @@
 
     private static final int MAX_SUGGESTION_SHORT_ENTRIES = 3;
     private static final int MAX_SUGGESTION_LONG_ENTRIES = 6;
+    private static final String MAX_SUGGESTION_LONG_ENTRIES_STRING =
+            Integer.valueOf(MAX_SUGGESTION_LONG_ENTRIES).toString();
 
     // make sure that these match the index of TABLE_NAMES
     private static final int URI_MATCH_BOOKMARKS = 0;
@@ -143,7 +149,8 @@
     // 15 -> 17 Set it up for the SearchManager
     // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
     // 18 -> 19 Remove labels table
-    private static final int DATABASE_VERSION = 19;
+    // 19 -> 20 Added thumbnail
+    private static final int DATABASE_VERSION = 20;
 
     // Regular expression which matches http://, followed by some stuff, followed by
     // optionally a trailing slash, all matched as separate groups.
@@ -215,7 +222,8 @@
                     "created LONG," +
                     "description TEXT," +
                     "bookmark INTEGER," +
-                    "favicon BLOB DEFAULT NULL" +
+                    "favicon BLOB DEFAULT NULL," +
+                    "thumbnail BLOB DEFAULT NULL" +
                     ");");
 
             final CharSequence[] bookmarks = mContext.getResources()
@@ -242,9 +250,12 @@
         @Override
         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
             Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
-                    + newVersion + ", which will destroy all old data");
+                    + newVersion);
             if (oldVersion == 18) {
                 db.execSQL("DROP TABLE IF EXISTS labels");
+            }
+            if (oldVersion <= 19) {
+                db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
             } else {
                 db.execSQL("DROP TABLE IF EXISTS bookmarks");
                 db.execSQL("DROP TABLE IF EXISTS searches");
@@ -257,6 +268,7 @@
     public boolean onCreate() {
         final Context context = getContext();
         mOpenHelper = new DatabaseHelper(context);
+        mBackupManager = new BackupManager(context);
         // we added "picasa web album" into default bookmarks for version 19.
         // To avoid erasing the bookmark table, we added it explicitly for
         // version 18 and 19 as in the other cases, we will erase the table.
@@ -471,22 +483,22 @@
                     case SUGGEST_COLUMN_ICON_1_ID:
                         if (mHistoryCount > mPos) {
                             if (mHistoryCursor.getInt(3) == 1) {
-                                return new Integer(
+                                return Integer.valueOf(
                                         R.drawable.ic_search_category_bookmark)
                                         .toString();
                             } else {
-                                return new Integer(
+                                return Integer.valueOf(
                                         R.drawable.ic_search_category_history)
                                         .toString();
                             }
                         } else {
-                            return new Integer(
+                            return Integer.valueOf(
                                     R.drawable.ic_search_category_suggest)
                                     .toString();
                         }
 
                     case SUGGEST_COLUMN_ICON_2_ID:
-                        return new String("0");
+                        return "0";
 
                     case SUGGEST_COLUMN_QUERY_ID:
                         if (mHistoryCount > mPos) {
@@ -637,7 +649,8 @@
                 myArgs = null;
             } else {
                 String like = selectionArgs[0] + "%";
-                if (selectionArgs[0].startsWith("http")) {
+                if (selectionArgs[0].startsWith("http")
+                        || selectionArgs[0].startsWith("file")) {
                     myArgs = new String[1];
                     myArgs[0] = like;
                     suggestSelection = selection;
@@ -655,8 +668,7 @@
 
             Cursor c = db.query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
                     SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
-                    ORDER_BY,
-                    (new Integer(MAX_SUGGESTION_LONG_ENTRIES)).toString());
+                    ORDER_BY, MAX_SUGGESTION_LONG_ENTRIES_STRING);
 
             if (match == URI_MATCH_BOOKMARKS_SUGGEST
                     || Regex.WEB_URL_PATTERN.matcher(selectionArgs[0]).matches()) {
@@ -729,6 +741,7 @@
 
     @Override
     public Uri insert(Uri url, ContentValues initialValues) {
+        boolean isBookmarkTable = false;
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         int match = URI_MATCHER.match(url);
@@ -742,6 +755,7 @@
                     uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
                             rowID);
                 }
+                isBookmarkTable = true;
                 break;
             }
 
@@ -764,6 +778,15 @@
             throw new IllegalArgumentException("Unknown URL");
         }
         getContext().getContentResolver().notifyChange(uri, null);
+
+        // Back up the new bookmark set if we just inserted one.
+        // A row created when bookmarks are added from scratch will have
+        // bookmark=1 in the initial value set.
+        if (isBookmarkTable
+                && initialValues.containsKey(BookmarkColumns.BOOKMARK)
+                && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
+            mBackupManager.dataChanged();
+        }
         return uri;
     }
 
@@ -776,20 +799,41 @@
             throw new IllegalArgumentException("Unknown URL");
         }
 
-        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+        // need to know whether it's the bookmarks table for a couple of reasons
+        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+        String id = null;
+
+        if (isBookmarkTable || 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);
             sb.append("_id = ");
-            sb.append(url.getPathSegments().get(1));
+            sb.append(id);
             where = sb.toString();
         }
 
+        ContentResolver cr = getContext().getContentResolver();
+
+        // we'lll need to back up the bookmark set if we are about to delete one
+        if (isBookmarkTable) {
+            Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+                    new String[] { BookmarkColumns.BOOKMARK },
+                    "_id = " + id, null, null);
+            if (cursor.moveToNext()) {
+                if (cursor.getInt(0) != 0) {
+                    // yep, this record is a bookmark
+                    mBackupManager.dataChanged();
+                }
+            }
+            cursor.close();
+        }
+
         int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
-        getContext().getContentResolver().notifyChange(url, null);
+        cr.notifyChange(url, null);
         return count;
     }
 
@@ -803,20 +847,59 @@
             throw new IllegalArgumentException("Unknown URL");
         }
 
-        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+        String id = null;
+        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+        boolean changingBookmarks = false;
+
+        if (isBookmarkTable || 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);
             sb.append("_id = ");
-            sb.append(url.getPathSegments().get(1));
+            sb.append(id);
             where = sb.toString();
         }
 
+        ContentResolver cr = getContext().getContentResolver();
+
+        // 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) {
+            // 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
+                Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+                        new String[] { BookmarkColumns.BOOKMARK },
+                        "_id = " + id, null, null);
+                if (cursor.moveToNext()) {
+                    changingBookmarks = (cursor.getInt(0) != 0);
+                }
+                cursor.close();
+            }
+
+            // if this *is* a bookmark row we're altering, we need to back it up.
+            if (changingBookmarks) {
+                mBackupManager.dataChanged();
+            }
+        }
+
         int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
-        getContext().getContentResolver().notifyChange(url, null);
+        cr.notifyChange(url, null);
         return ret;
     }
 
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index a5e23c9..3ed6cf0 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -20,16 +20,22 @@
 
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.pm.ActivityInfo;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
 import android.webkit.CookieManager;
+import android.webkit.GeolocationPermissions;
 import android.webkit.WebView;
 import android.webkit.WebViewDatabase;
 import android.webkit.WebIconDatabase;
 import android.webkit.WebSettings;
+import android.webkit.WebStorage;
 import android.preference.PreferenceManager;
 import android.provider.Browser;
 
+import java.util.Set;
 import java.util.HashMap;
 import java.util.Observable;
 
@@ -48,7 +54,7 @@
  */
 class BrowserSettings extends Observable {
 
-    // Public variables for settings
+    // Private variables for settings
     // NOTE: these defaults need to be kept in sync with the XML
     // until the performance of PreferenceManager.setDefaultValues()
     // is improved.
@@ -65,7 +71,21 @@
     private String homeUrl = "";
     private boolean loginInitialized = false;
     private boolean autoFitPage = true;
+    private boolean landscapeOnly = false;
     private boolean showDebugSettings = false;
+    private String databasePath; // default value set in loadFromDb()
+    private boolean databaseEnabled = true;
+    private long webStorageDefaultQuota = 5 * 1024 * 1024;
+    // The Browser always enables Application Caches.
+    private boolean appCacheEnabled = true;
+    private String appCachePath;  // default value set in loadFromDb().
+    private long appCacheMaxSize = Long.MAX_VALUE;
+    private WebStorageSizeManager webStorageSizeManager;
+    private boolean domStorageEnabled = true;
+    private String jsFlags = "";
+    private boolean geolocationEnabled = true;
+
+    private final static String TAG = "BrowserSettings";
 
     // Development settings
     public WebSettings.LayoutAlgorithm layoutAlgorithm =
@@ -75,6 +95,11 @@
     private boolean tracing = false;
     private boolean lightTouch = false;
     private boolean navDump = false;
+
+    // By default the error console is shown once the user navigates to about:debug.
+    // The setting can be then toggled from the settings menu.
+    private boolean showConsole = true;
+
     // Browser only settings
     private boolean doFlick = false;
 
@@ -97,22 +122,27 @@
             "privacy_clear_form_data";
     public final static String PREF_CLEAR_PASSWORDS =
             "privacy_clear_passwords";
+    public final static String PREF_DEFAULT_QUOTA =
+            "webstorage_default_quota";
     public final static String PREF_EXTRAS_RESET_DEFAULTS =
             "reset_default_preferences";
     public final static String PREF_DEBUG_SETTINGS = "debug_menu";
     public final static String PREF_GEARS_SETTINGS = "gears_settings";
+    public final static String PREF_WEBSITE_SETTINGS = "website_settings";
     public final static String PREF_TEXT_SIZE = "text_size";
     public final static String PREF_DEFAULT_ZOOM = "default_zoom";
     public final static String PREF_DEFAULT_TEXT_ENCODING =
             "default_text_encoding";
+    public final static String PREF_CLEAR_LOCATION_ACCESS =
+            "privacy_clear_location_access";
 
     private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " +
-            "U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.18 (KHTML, " +
-            "like Gecko) Version/3.1.2 Safari/525.20.1";
+            "U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, " +
+            "like Gecko) Version/4.0 Safari/530.17";
 
     private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " +
-            "CPU iPhone OS 2_2 like Mac OS X; en-us) AppleWebKit/525.18.1 " +
-            "(KHTML, like Gecko) Version/3.1.1 Mobile/5G77 Safari/525.20";
+            "CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 " +
+            "(KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16";
 
     // Value to truncate strings when adding them to a TextView within
     // a ListView
@@ -157,7 +187,6 @@
             s.setLoadsImagesAutomatically(b.loadsImagesAutomatically);
             s.setJavaScriptEnabled(b.javaScriptEnabled);
             s.setPluginsEnabled(b.pluginsEnabled);
-            s.setPluginsPath(b.pluginsPath);
             s.setJavaScriptCanOpenWindowsAutomatically(
                     b.javaScriptCanOpenWindowsAutomatically);
             s.setDefaultTextEncodingName(b.defaultTextEncodingName);
@@ -178,6 +207,24 @@
             s.setSupportMultipleWindows(true);
             // Turn off file access
             s.setAllowFileAccess(false);
+
+            s.setDatabasePath(b.databasePath);
+            s.setDatabaseEnabled(b.databaseEnabled);
+            s.setDomStorageEnabled(b.domStorageEnabled);
+            s.setWebStorageDefaultQuota(b.webStorageDefaultQuota);
+
+            // Turn on Application Caches.
+            s.setAppCachePath(b.appCachePath);
+            s.setAppCacheEnabled(b.appCacheEnabled);
+            s.setAppCacheMaxSize(b.appCacheMaxSize);
+
+            // Enable/Disable the error console.
+            b.mTabControl.getBrowserActivity().setShouldShowErrorConsole(
+                    b.showDebugSettings && b.showConsole);
+
+            // Configure the Geolocation permissions manager to deny all
+            // permission requests if Geolocation is disabled in the browser.
+            // TODO(steveblock): Implement
         }
     }
 
@@ -197,6 +244,16 @@
         // Set the default value for the plugins path to the application's
         // local directory.
         pluginsPath = ctx.getDir("plugins", 0).getPath();
+        // Set the default value for the Application Caches path.
+        appCachePath = ctx.getDir("appcache", 0).getPath();
+        // Determine the maximum size of the application cache.
+        webStorageSizeManager = new WebStorageSizeManager(
+                ctx,
+                new WebStorageSizeManager.StatFsDiskInfo(appCachePath),
+                new WebStorageSizeManager.WebKitAppCacheInfo(appCachePath));
+        appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
+        // Set the default value for the Database path.
+        databasePath = ctx.getDir("databases", 0).getPath();
 
         homeUrl = getFactoryResetHomeUrl(ctx);
 
@@ -219,6 +276,15 @@
         pluginsEnabled = p.getBoolean("enable_plugins",
                 pluginsEnabled);
         pluginsPath = p.getString("plugins_path", pluginsPath);
+        databasePath = p.getString("database_path", databasePath);
+        databaseEnabled = p.getBoolean("enable_database", databaseEnabled);
+        webStorageDefaultQuota = Long.parseLong(p.getString(PREF_DEFAULT_QUOTA,
+                String.valueOf(webStorageDefaultQuota)));
+        appCacheEnabled = p.getBoolean("enable_appcache",
+                appCacheEnabled);
+        domStorageEnabled = p.getBoolean("enable_domstorage",
+                domStorageEnabled);
+        appCachePath = p.getString("appcache_path", appCachePath);
         javaScriptCanOpenWindowsAutomatically = !p.getBoolean(
             "block_popup_windows",
             !javaScriptCanOpenWindowsAutomatically);
@@ -238,6 +304,14 @@
         zoomDensity = WebSettings.ZoomDensity.valueOf(
                 p.getString(PREF_DEFAULT_ZOOM, zoomDensity.name()));
         autoFitPage = p.getBoolean("autofit_pages", autoFitPage);
+        boolean landscapeOnlyTemp =
+                p.getBoolean("landscape_only", landscapeOnly);
+        if (landscapeOnlyTemp != landscapeOnly) {
+            landscapeOnly = landscapeOnlyTemp;
+            mTabControl.getBrowserActivity().setRequestedOrientation(
+                    landscapeOnly ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+                    : ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
+        }
         useWideViewPort = true; // use wide view port for either setting
         if (autoFitPage) {
             layoutAlgorithm = WebSettings.LayoutAlgorithm.NARROW_COLUMNS;
@@ -274,6 +348,21 @@
             doFlick = p.getBoolean("enable_flick", doFlick);
             userAgent = Integer.parseInt(p.getString("user_agent", "0"));
         }
+        // JS flags is loaded from DB even if showDebugSettings is false,
+        // so that it can be set once and be effective all the time.
+        jsFlags = p.getString("js_engine_flags", "");
+
+        // Read the setting for showing/hiding the JS Console always so that should the
+        // user enable debug settings, we already know if we should show the console.
+        // The user will never see the console unless they navigate to about:debug,
+        // regardless of the setting we read here. This setting is only used after debug
+        // is enabled.
+        showConsole = p.getBoolean("javascript_console", showConsole);
+        mTabControl.getBrowserActivity().setShouldShowErrorConsole(
+                showDebugSettings && showConsole);
+
+        geolocationEnabled = p.getBoolean("enable_geolocation", geolocationEnabled);
+
         update();
     }
 
@@ -285,6 +374,14 @@
         return homeUrl;
     }
 
+    public String getJsFlags() {
+        return jsFlags;
+    }
+
+    public WebStorageSizeManager getWebStorageSizeManager() {
+        return webStorageSizeManager;
+    }
+
     public void setHomePage(Context context, String url) {
         Editor ed = PreferenceManager.
                 getDefaultSharedPreferences(context).edit();
@@ -436,14 +533,39 @@
         db.clearHttpAuthUsernamePassword();
     }
 
-    /*package*/ void resetDefaultPreferences(Context context) {
+    private void maybeDisableWebsiteSettings(Context context) {
+        Set webStorageOrigins = WebStorage.getInstance().getOrigins();
+        Set geolocationOrigins =
+                 GeolocationPermissions.getInstance().getOrigins();
+        if (((webStorageOrigins == null) || webStorageOrigins.isEmpty()) &&
+            ((geolocationOrigins == null) || geolocationOrigins.isEmpty())) {
+            PreferenceActivity activity = (PreferenceActivity) context;
+            PreferenceScreen screen = (PreferenceScreen)
+                activity.findPreference(BrowserSettings.PREF_WEBSITE_SETTINGS);
+            screen.setEnabled(false);
+        }
+    }
+
+    /*package*/ void clearDatabases(Context context) {
+        WebStorage.getInstance().deleteAllData();
+        maybeDisableWebsiteSettings(context);
+    }
+
+    /*package*/ void clearLocationAccess(Context context) {
+        GeolocationPermissions.getInstance().clearAll();
+        maybeDisableWebsiteSettings(context);
+    }
+
+    /*package*/ void resetDefaultPreferences(Context ctx) {
         SharedPreferences p =
-            PreferenceManager.getDefaultSharedPreferences(context);
+            PreferenceManager.getDefaultSharedPreferences(ctx);
         p.edit().clear().commit();
-        PreferenceManager.setDefaultValues(context, R.xml.browser_preferences,
+        PreferenceManager.setDefaultValues(ctx, R.xml.browser_preferences,
                 true);
         // reset homeUrl
-        setHomePage(context, getFactoryResetHomeUrl(context));
+        setHomePage(ctx, getFactoryResetHomeUrl(ctx));
+        // reset appcache max size
+        appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
     }
 
     private String getFactoryResetHomeUrl(Context context) {
diff --git a/src/com/android/browser/BrowserYesNoPreference.java b/src/com/android/browser/BrowserYesNoPreference.java
index 65cde71..e380e57 100644
--- a/src/com/android/browser/BrowserYesNoPreference.java
+++ b/src/com/android/browser/BrowserYesNoPreference.java
@@ -38,6 +38,7 @@
             Context context = getContext();
             if (BrowserSettings.PREF_CLEAR_CACHE.equals(getKey())) {
                 BrowserSettings.getInstance().clearCache(context);
+                BrowserSettings.getInstance().clearDatabases(context);
             } else if (BrowserSettings.PREF_CLEAR_COOKIES.equals(getKey())) {
                 BrowserSettings.getInstance().clearCookies(context);
             } else if (BrowserSettings.PREF_CLEAR_HISTORY.equals(getKey())) {
@@ -50,6 +51,9 @@
                     getKey())) {
                 BrowserSettings.getInstance().resetDefaultPreferences(context);
                 setEnabled(true);
+            } else if (BrowserSettings.PREF_CLEAR_LOCATION_ACCESS.equals(
+                    getKey())) {
+                BrowserSettings.getInstance().clearLocationAccess(context);
             }
         }
     }
diff --git a/src/com/android/browser/ErrorConsoleView.java b/src/com/android/browser/ErrorConsoleView.java
new file mode 100644
index 0000000..56f663b
--- /dev/null
+++ b/src/com/android/browser/ErrorConsoleView.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2009 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.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import java.util.Vector;
+
+/* package */ class ErrorConsoleView extends LinearLayout {
+
+    /**
+     * Define some constants to describe the visibility of the error console.
+     */
+    public static final int SHOW_MINIMIZED = 0;
+    public static final int SHOW_MAXIMIZED = 1;
+    public static final int SHOW_NONE      = 2;
+
+    private TextView mConsoleHeader;
+    private ErrorConsoleListView mErrorList;
+    private LinearLayout mEvalJsViewGroup;
+    private EditText mEvalEditText;
+    private Button mEvalButton;
+    private WebView mWebView;
+    private int mCurrentShowState = SHOW_NONE;
+
+    private boolean mSetupComplete = false;
+
+    // Before we've been asked to display the console, cache any messages that should
+    // be added to the console. Then when we do display the console, add them to the view
+    // then.
+    private Vector<ErrorConsoleMessage> mErrorMessageCache;
+
+    public ErrorConsoleView(Context context) {
+        super(context);
+    }
+
+    public ErrorConsoleView(Context context, AttributeSet attributes) {
+        super(context, attributes);
+    }
+
+    private void commonSetupIfNeeded() {
+        if (mSetupComplete) {
+            return;
+        }
+
+        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.error_console, this);
+
+        // Get references to each ui element.
+        mConsoleHeader = (TextView) findViewById(R.id.error_console_header_id);
+        mErrorList = (ErrorConsoleListView) findViewById(R.id.error_console_list_id);
+        mEvalJsViewGroup = (LinearLayout) findViewById(R.id.error_console_eval_view_group_id);
+        mEvalEditText = (EditText) findViewById(R.id.error_console_eval_text_id);
+        mEvalButton = (Button) findViewById(R.id.error_console_eval_button_id);
+
+        mEvalButton.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                // Send the javascript to be evaluated to webkit as a javascript: url
+                // TODO: Can we expose access to webkit's JS interpreter here and evaluate it that
+                // way? Note that this is called on the UI thread so we will need to post a message
+                // to the WebCore thread to implement this.
+                if (mWebView != null) {
+                    mWebView.loadUrl("javascript:" + mEvalEditText.getText());
+                }
+
+                mEvalEditText.setText("");
+            }
+        });
+
+        // Make clicking on the console title bar min/maximse it.
+        mConsoleHeader.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                if (mCurrentShowState == SHOW_MINIMIZED) {
+                    showConsole(SHOW_MAXIMIZED);
+                } else {
+                    showConsole(SHOW_MINIMIZED);
+                }
+            }
+        });
+
+        // Add any cached messages to the list now that we've assembled the view.
+        if (mErrorMessageCache != null) {
+            for (ErrorConsoleMessage msg : mErrorMessageCache) {
+                mErrorList.addErrorMessage(msg.getMessage(), msg.getSourceID(), msg.getLineNumber());
+            }
+            mErrorMessageCache.clear();
+        }
+
+        mSetupComplete = true;
+    }
+
+    /**
+     * Adds a message to the set of messages the console uses.
+     */
+    public void addErrorMessage(String msg, String sourceId, int lineNumber) {
+        if (mSetupComplete) {
+            mErrorList.addErrorMessage(msg, sourceId, lineNumber);
+        } else {
+            if (mErrorMessageCache == null) {
+                mErrorMessageCache = new Vector<ErrorConsoleMessage>();
+            }
+            mErrorMessageCache.add(new ErrorConsoleMessage(msg, sourceId, lineNumber));
+        }
+    }
+
+    /**
+     * Removes all error messages from the console.
+     */
+    public void clearErrorMessages() {
+        if (mSetupComplete) {
+            mErrorList.clearErrorMessages();
+        } else if (mErrorMessageCache != null) {
+            mErrorMessageCache.clear();
+        }
+    }
+
+    /**
+     * Returns the current number of errors displayed in the console.
+     */
+    public int numberOfErrors() {
+        if (mSetupComplete) {
+            return mErrorList.getCount();
+        } else {
+            return (mErrorMessageCache == null) ? 0 : mErrorMessageCache.size();
+        }
+    }
+
+    /**
+     * Sets the webview that this console is associated with. Currently this is used so
+     * we can call into webkit to evaluate JS expressions in the console.
+     */
+    public void setWebView(WebView webview) {
+        mWebView = webview;
+    }
+
+    /**
+     * Sets the visibility state of the console.
+     */
+    public void showConsole(int show_state) {
+        commonSetupIfNeeded();
+        switch (show_state) {
+            case SHOW_MINIMIZED:
+                mConsoleHeader.setVisibility(View.VISIBLE);
+                mConsoleHeader.setText(R.string.error_console_header_text_minimized);
+                mErrorList.setVisibility(View.GONE);
+                mEvalJsViewGroup.setVisibility(View.GONE);
+                break;
+
+            case SHOW_MAXIMIZED:
+                mConsoleHeader.setVisibility(View.VISIBLE);
+                mConsoleHeader.setText(R.string.error_console_header_text_maximized);
+                mErrorList.setVisibility(View.VISIBLE);
+                mEvalJsViewGroup.setVisibility(View.VISIBLE);
+                break;
+
+            case SHOW_NONE:
+                mConsoleHeader.setVisibility(View.GONE);
+                mErrorList.setVisibility(View.GONE);
+                mEvalJsViewGroup.setVisibility(View.GONE);
+                break;
+        }
+        mCurrentShowState = show_state;
+    }
+
+    /**
+     * Returns the current visibility state of the console.
+     */
+    public int getShowState() {
+        if (mSetupComplete) {
+            return mCurrentShowState;
+        } else {
+            return SHOW_NONE;
+        }
+    }
+
+    /**
+     * This class extends ListView to implement the View that will actually display the set of
+     * errors encountered on the current page.
+     */
+    private static class ErrorConsoleListView extends ListView {
+        // An adapter for this View that contains a list of error messages.
+        private ErrorConsoleMessageList mConsoleMessages;
+
+        public ErrorConsoleListView(Context context, AttributeSet attributes) {
+            super(context, attributes);
+            mConsoleMessages = new ErrorConsoleMessageList(context);
+            setAdapter(mConsoleMessages);
+        }
+
+        public void addErrorMessage(String msg, String sourceId, int lineNumber) {
+            mConsoleMessages.add(msg, sourceId, lineNumber);
+            setSelection(mConsoleMessages.getCount());
+        }
+
+        public void clearErrorMessages() {
+            mConsoleMessages.clear();
+        }
+
+        /**
+         * This class is an adapter for ErrorConsoleListView that contains the error console
+         * message data.
+         */
+        private class ErrorConsoleMessageList extends android.widget.BaseAdapter
+                implements android.widget.ListAdapter {
+
+            private Vector<ErrorConsoleMessage> mMessages;
+            private LayoutInflater mInflater;
+
+            public ErrorConsoleMessageList(Context context) {
+                mMessages = new Vector<ErrorConsoleMessage>();
+                mInflater = (LayoutInflater)context.getSystemService(
+                        Context.LAYOUT_INFLATER_SERVICE);
+            }
+
+            /**
+             * Add a new message to the list and update the View.
+             */
+            public void add(String msg, String sourceID, int lineNumber) {
+                mMessages.add(new ErrorConsoleMessage(msg, sourceID, lineNumber));
+                notifyDataSetChanged();
+            }
+
+            /**
+             * Remove all messages from the list and update the view.
+             */
+            public void clear() {
+                mMessages.clear();
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public boolean areAllItemsEnabled() {
+                return false;
+            }
+
+            @Override
+            public boolean isEnabled(int position) {
+                return false;
+            }
+
+            public long getItemId(int position) {
+                return position;
+            }
+
+            public Object getItem(int position) {
+                return mMessages.get(position);
+            }
+
+            public int getCount() {
+                return mMessages.size();
+            }
+
+            @Override
+            public boolean hasStableIds() {
+                return true;
+            }
+
+            /**
+             * Constructs a TwoLineListItem for the error at position.
+             */
+            public View getView(int position, View convertView, ViewGroup parent) {
+                View view;
+                ErrorConsoleMessage error = mMessages.get(position);
+
+                if (error == null) {
+                    return null;
+                }
+
+                if (convertView == null) {
+                    view = mInflater.inflate(android.R.layout.two_line_list_item, parent, false);
+                } else {
+                    view = convertView;
+                }
+
+                TextView headline = (TextView) view.findViewById(android.R.id.text1);
+                TextView subText = (TextView) view.findViewById(android.R.id.text2);
+                headline.setText(error.getSourceID() + ":" + error.getLineNumber());
+                subText.setText(error.getMessage());
+                return view;
+            }
+
+        }
+    }
+
+    /**
+     * This class holds the data for a single error message in the console.
+     */
+    private static class ErrorConsoleMessage {
+        private String mMessage;
+        private String mSourceID;
+        private int mLineNumber;
+
+        public ErrorConsoleMessage(String msg, String sourceID, int lineNumber) {
+            mMessage = msg;
+            mSourceID = sourceID;
+            mLineNumber = lineNumber;
+        }
+
+        public String getMessage() {
+            return mMessage;
+        }
+
+        public String getSourceID() {
+            return mSourceID;
+        }
+
+        public int getLineNumber() {
+            return mLineNumber;
+        }
+    }
+}
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
index 8578643..c585dbb 100644
--- a/src/com/android/browser/FetchUrlMimeType.java
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -58,7 +58,7 @@
         mValues = values[0];
 
         // Check to make sure we have a URI to download
-        String uri = mValues.getAsString(Downloads.URI);
+        String uri = mValues.getAsString(Downloads.COLUMN_URI);
         if (uri == null || uri.length() == 0) {
             return null;
         }
@@ -66,15 +66,15 @@
         // User agent is likely to be null, though the AndroidHttpClient
         // seems ok with that.
         AndroidHttpClient client = AndroidHttpClient.newInstance(
-                mValues.getAsString(Downloads.USER_AGENT));
+                mValues.getAsString(Downloads.COLUMN_USER_AGENT));
         HttpHead request = new HttpHead(uri);
 
-        String cookie = mValues.getAsString(Downloads.COOKIE_DATA);
+        String cookie = mValues.getAsString(Downloads.COLUMN_COOKIE_DATA);
         if (cookie != null && cookie.length() > 0) {
             request.addHeader("Cookie", cookie);
         }
 
-        String referer = mValues.getAsString(Downloads.REFERER);
+        String referer = mValues.getAsString(Downloads.COLUMN_REFERER);
         if (referer != null && referer.length() > 0) {
             request.addHeader("Referer", referer);
         }
@@ -111,19 +111,19 @@
    @Override
     public void onPostExecute(String mimeType) {
        if (mimeType != null) {
-           String url = mValues.getAsString(Downloads.URI);
+           String url = mValues.getAsString(Downloads.COLUMN_URI);
            if (mimeType.equalsIgnoreCase("text/plain") ||
                    mimeType.equalsIgnoreCase("application/octet-stream")) {
                String newMimeType =
                        MimeTypeMap.getSingleton().getMimeTypeFromExtension(
                            MimeTypeMap.getFileExtensionFromUrl(url));
                if (newMimeType != null) {
-                   mValues.put(Downloads.MIMETYPE, newMimeType);
+                   mValues.put(Downloads.COLUMN_MIME_TYPE, newMimeType);
                }
            }
            String filename = URLUtil.guessFileName(url,
                    null, mimeType);
-           mValues.put(Downloads.FILENAME_HINT, filename);
+           mValues.put(Downloads.COLUMN_FILE_NAME_HINT, filename);
        }
 
        // Start the download
diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java
index 6e9574c..2049bd0 100644
--- a/src/com/android/browser/FindDialog.java
+++ b/src/com/android/browser/FindDialog.java
@@ -42,7 +42,6 @@
     private BrowserActivity mBrowserActivity;
     
     // Views with which the user can interact.
-    private View            mOk;
     private EditText        mEditText;
     private View            mNextButton;
     private View            mPrevButton;
@@ -129,7 +128,6 @@
         
         button = findViewById(R.id.done);
         button.setOnClickListener(mFindCancelListener);
-        mOk = button;
         
         mMatches = (TextView) findViewById(R.id.matches);
         mMatchesView = findViewById(R.id.matches_view);
@@ -143,23 +141,14 @@
         mBrowserActivity.closeFind();
         mWebView.clearMatches();
     }
-    
+
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        int code = event.getKeyCode();
-        boolean up = event.getAction() == KeyEvent.ACTION_UP;
-        switch (code) {
-            case KeyEvent.KEYCODE_DPAD_CENTER:
-            case KeyEvent.KEYCODE_ENTER:
-                if (!mEditText.hasFocus()) {
-                    break;
-                }
-                if (up) {
-                    findNext();
-                }
-                return true;
-            default:
-                break;
+        if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
+                && event.getAction() == KeyEvent.ACTION_UP
+                && mEditText.hasFocus()) {
+            findNext();
+            return true;
         }
         return super.dispatchKeyEvent(event);
     }
diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java
index 55e43f0..e8f15b1 100644
--- a/src/com/android/browser/HistoryItem.java
+++ b/src/com/android/browser/HistoryItem.java
@@ -17,23 +17,13 @@
  
 package com.android.browser;
 
-import android.content.ContentResolver;
-import android.content.ContentUris;
-import android.content.ContentValues;
 import android.content.Context;
-import android.database.Cursor;
 import android.graphics.Bitmap;
-import android.net.Uri;
 import android.provider.Browser;
-import android.util.Log;
 import android.view.View;
-import android.webkit.WebIconDatabase;
 import android.widget.CompoundButton;
 import android.widget.ImageView;
 import android.widget.TextView;
-import android.widget.Toast;
-
-import java.util.Date;
 
 /**
  *  Layout representing a history item in the classic history viewer.
@@ -54,61 +44,18 @@
         mListener = new CompoundButton.OnCheckedChangeListener() {
             public void onCheckedChanged(CompoundButton buttonView,
                     boolean isChecked) {
-                ContentResolver cr = mContext.getContentResolver();
-                Cursor cursor = cr.query(
-                        Browser.BOOKMARKS_URI,
-                        Browser.HISTORY_PROJECTION,
-                        "url = ?",
-                        new String[] { mUrl },
-                        null);
-                boolean first = cursor.moveToFirst();
-                // Should be in the database no matter what
-                if (!first) {
-                    throw new AssertionError("URL is not in the database!");
-                }
                 if (isChecked) {
-                    // Add to bookmarks
-                    // FIXME: Share code with AddBookmarkPage.java
-                    ContentValues map = new ContentValues();
-                    map.put(Browser.BookmarkColumns.CREATED,
-                            new Date().getTime());
-                    map.put(Browser.BookmarkColumns.TITLE, getName());
-                    map.put(Browser.BookmarkColumns.BOOKMARK, 1);
-                    try {
-                        cr.update(Browser.BOOKMARKS_URI, map, 
-                                "_id = " + cursor.getInt(0), null);
-                    } catch (IllegalStateException e) {
-                        Log.e("HistoryItem", "no database!");
-                    }
-                    WebIconDatabase.getInstance().retainIconForPageUrl(mUrl);
-                    // catch IllegalStateException?
-                    Toast.makeText(mContext, R.string.added_to_bookmarks,
-                            Toast.LENGTH_LONG).show();
+                    Bookmarks.addBookmark(mContext,
+                            mContext.getContentResolver(), mUrl, getName(), true);
                 } else {
-                    // Remove from bookmarks
-                    // FIXME: This code should be shared with
-                    // BrowserBookmarksAdapter.java
-                    WebIconDatabase.getInstance().releaseIconForPageUrl(mUrl);
-                    Uri uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
-                            cursor.getInt(Browser.HISTORY_PROJECTION_ID_INDEX));
-                    // It is no longer a bookmark, but it is still a visited
-                    // site.
-                    ContentValues values = new ContentValues();
-                    values.put(Browser.BookmarkColumns.BOOKMARK, 0);
-                    try {
-                        cr.update(uri, values, null, null);
-                    } catch (IllegalStateException e) {
-                        Log.e("HistoryItem", "no database!");
-                    }
-                    Toast.makeText(mContext, R.string.removed_from_bookmarks,
-                            Toast.LENGTH_LONG).show();
+                    Bookmarks.removeFromBookmarks(mContext,
+                            mContext.getContentResolver(), mUrl);
                 }
-                cursor.deactivate();
             }
         };
     }
     
-    void copyTo(HistoryItem item) {
+    /* package */ void copyTo(HistoryItem item) {
         item.mTextView.setText(mTextView.getText());
         item.mUrlText.setText(mUrlText.getText());
         item.setIsBookmark(mStar.isChecked());
@@ -116,10 +63,17 @@
     }
 
     /**
+     * Whether or not this item represents a bookmarked site
+     */
+    /* package */ boolean isBookmark() {
+        return mStar.isChecked();
+    }
+
+    /**
      *  Set whether or not this represents a bookmark, and make sure the star
      *  behaves appropriately.
      */
-    void setIsBookmark(boolean isBookmark) {
+    /* package */ void setIsBookmark(boolean isBookmark) {
         mStar.setOnCheckedChangeListener(null);
         mStar.setChecked(isBookmark);
         mStar.setOnCheckedChangeListener(mListener);
diff --git a/src/com/android/browser/MostVisitedActivity.java b/src/com/android/browser/MostVisitedActivity.java
index 704ee27..90052d3 100644
--- a/src/com/android/browser/MostVisitedActivity.java
+++ b/src/com/android/browser/MostVisitedActivity.java
@@ -35,6 +35,7 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
+import android.view.ViewStub;
 
 import java.util.Vector;
 
@@ -50,8 +51,7 @@
                 .addListener(new IconReceiver());
         setListAdapter(mAdapter);
         ListView list = getListView();
-        LayoutInflater factory = LayoutInflater.from(this);
-        View v = factory.inflate(R.layout.empty_history, null);
+        View v = new ViewStub(this, R.layout.empty_history);
         addContentView(v, new LayoutParams(LayoutParams.FILL_PARENT,
                 LayoutParams.FILL_PARENT));
         list.setEmptyView(v);
@@ -84,9 +84,9 @@
         private Vector<DataSetObserver> mObservers;
         private Cursor mCursor;
         // These correspond with projection below.
-        private final int mUrlIndex = 0;
-        private final int mTitleIndex = 1;
-        private final int mBookmarkIndex = 2;
+        private static final int mUrlIndex = 0;
+        private static final int mTitleIndex = 1;
+        private static final int mBookmarkIndex = 2;
 
         MyAdapter() {
             mObservers = new Vector<DataSetObserver>();
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
index 575be8d..c7c3e3f 100644
--- a/src/com/android/browser/TabControl.java
+++ b/src/com/android/browser/TabControl.java
@@ -195,6 +195,8 @@
         // url has not changed.
         private String mOriginalUrl;
 
+        private ErrorConsoleView mErrorConsole;
+
         // Construct a new tab
         private Tab(WebView w, boolean closeOnExit, String appId, String url) {
             mMainView = w;
@@ -388,6 +390,28 @@
     }
 
     /**
+     * Return the current tab's error console. Creates the console if createIfNEcessary
+     * is true and we haven't already created the console.
+     * @param createIfNecessary Flag to indicate if the console should be created if it has
+     *                          not been already.
+     * @return The current tab's error console, or null if one has not been created and
+     *         createIfNecessary is false.
+     */
+    ErrorConsoleView getCurrentErrorConsole(boolean createIfNecessary) {
+        Tab t = getTab(mCurrentTab);
+        if (t == null) {
+            return null;
+        }
+
+        if (createIfNecessary && t.mErrorConsole == null) {
+            t.mErrorConsole = new ErrorConsoleView(mActivity);
+            t.mErrorConsole.setWebView(t.mMainView);
+        }
+
+        return t.mErrorConsole;
+    }
+
+    /**
      * Return the current tab's top-level WebView. This can return a subwindow
      * if one exists.
      * @return The top-level WebView of the current tab.
@@ -446,6 +470,9 @@
      * @return index of Tab or -1 if not found
      */
     int getTabIndex(Tab tab) {
+        if (tab == null) {
+            return -1;
+        }
         return mTabs.indexOf(tab);
     }
 
@@ -681,11 +708,11 @@
             return;
         }
 
-        // free the WebView cache
-        Log.w(LOGTAG, "Free WebView cache");
+        // free the WebView's unused memory (this includes the cache)
+        Log.w(LOGTAG, "Free WebView's unused memory and cache");
         WebView view = getCurrentWebView();
         if (view != null) {
-            view.clearCache(false);
+            view.freeMemory();
         }
         // force a gc
         System.gc();
@@ -801,6 +828,45 @@
         return null;
     }
 
+    // This method checks if a non-app tab (one created within the browser)
+    // matches the given url.
+    private boolean tabMatchesUrl(Tab t, String url) {
+        if (t.mAppId != null) {
+            return false;
+        } else if (t.mMainView == null) {
+            return false;
+        } else if (url.equals(t.mMainView.getUrl()) ||
+                url.equals(t.mMainView.getOriginalUrl())) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Return the tab that has no app id associated with it and the url of the
+     * tab matches the given url.
+     * @param url The url to search for.
+     */
+    Tab findUnusedTabWithUrl(String url) {
+        if (url == null) {
+            return null;
+        }
+        // Check the current tab first.
+        Tab t = getCurrentTab();
+        if (t != null && tabMatchesUrl(t, url)) {
+            return t;
+        }
+        // Now check all the rest.
+        final int size = getTabCount();
+        for (int i = 0; i < size; i++) {
+            t = getTab(i);
+            if (tabMatchesUrl(t, url)) {
+                return t;
+            }
+        }
+        return null;
+    }
+
     /**
      * Recreate the main WebView of the given tab. Returns true if the WebView
      * was deleted.
@@ -865,6 +931,48 @@
         return setCurrentTab(newTab, false);
     }
 
+    /*package*/ void pauseCurrentTab() {
+        Tab t = getCurrentTab();
+        if (t != null) {
+            t.mMainView.onPause();
+            if (t.mSubView != null) {
+                t.mSubView.onPause();
+            }
+        }
+    }
+
+    /*package*/ void resumeCurrentTab() {
+        Tab t = getCurrentTab();
+        if (t != null) {
+            t.mMainView.onResume();
+            if (t.mSubView != null) {
+                t.mSubView.onResume();
+            }
+        }
+    }
+
+    private void putViewInForeground(WebView v, WebViewClient vc,
+                                     WebChromeClient cc) {
+        v.setWebViewClient(vc);
+        v.setWebChromeClient(cc);
+        v.setOnCreateContextMenuListener(mActivity);
+        v.setDownloadListener(mActivity);
+        v.onResume();
+    }
+
+    private void putViewInBackground(WebView v) {
+        // Set an empty callback so that default actions are not triggered.
+        v.setWebViewClient(mEmptyClient);
+        v.setWebChromeClient(mBackgroundChromeClient);
+        v.setOnCreateContextMenuListener(null);
+        // Leave the DownloadManager attached so that downloads can start in
+        // 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.
+        v.setDownloadListener(mActivity);
+        v.onPause();
+    }
+
     /**
      * If force is true, this method skips the check for newTab == current.
      */
@@ -890,7 +998,6 @@
         mTabQueue.add(newTab);
 
         WebView mainView;
-        WebView subView;
 
         // Display the new current tab
         mCurrentTab = mTabs.indexOf(newTab);
@@ -900,17 +1007,12 @@
             // Same work as in createNewTab() except don't do new Tab()
             newTab.mMainView = mainView = createNewWebView();
         }
-        mainView.setWebViewClient(mActivity.getWebViewClient());
-        mainView.setWebChromeClient(mActivity.getWebChromeClient());
-        mainView.setOnCreateContextMenuListener(mActivity);
-        mainView.setDownloadListener(mActivity);
+        putViewInForeground(mainView, mActivity.getWebViewClient(),
+                            mActivity.getWebChromeClient());
         // Add the subwindow if it exists
         if (newTab.mSubViewContainer != null) {
-            subView = newTab.mSubView;
-            subView.setWebViewClient(newTab.mSubViewClient);
-            subView.setWebChromeClient(newTab.mSubViewChromeClient);
-            subView.setOnCreateContextMenuListener(mActivity);
-            subView.setDownloadListener(mActivity);
+            putViewInForeground(newTab.mSubView, newTab.mSubViewClient,
+                                newTab.mSubViewChromeClient);
         }
         if (needRestore) {
             // Have to finish setCurrentTab work before calling restoreState
@@ -925,23 +1027,9 @@
      * Put the tab in the background using all the empty/background clients.
      */
     private void putTabInBackground(Tab t) {
-        WebView mainView = t.mMainView;
-        // Set an empty callback so that default actions are not triggered.
-        mainView.setWebViewClient(mEmptyClient);
-        mainView.setWebChromeClient(mBackgroundChromeClient);
-        mainView.setOnCreateContextMenuListener(null);
-        // Leave the DownloadManager attached so that downloads can start in
-        // 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.
-        mainView.setDownloadListener(mActivity);
-        WebView subView = t.mSubView;
-        if (subView != null) {
-            // Set an empty callback so that default actions are not triggered.
-            subView.setWebViewClient(mEmptyClient);
-            subView.setWebChromeClient(mBackgroundChromeClient);
-            subView.setOnCreateContextMenuListener(null);
-            subView.setDownloadListener(mActivity);
+        putViewInBackground(t.mMainView);
+        if (t.mSubView != null) {
+            putViewInBackground(t.mSubView);
         }
     }
 
@@ -1041,6 +1129,11 @@
         data.mScale = w.getScale();
         data.mScrollX = w.getScrollX();
         data.mScrollY = w.getScrollY();
+
+        // Remember the old picture if possible.
+        if (t.mPickerData != null) {
+            data.mPicture = t.mPickerData.mPicture;
+        }
         t.mPickerData = data;
     }
     
diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java
new file mode 100644
index 0000000..f534a03
--- /dev/null
+++ b/src/com/android/browser/TitleBar.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2009 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.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.webkit.WebView;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+public class TitleBar extends LinearLayout {
+    private TextView        mTitle;
+    private TextView        mUrl;
+    private ImageView       mLftButton;
+    private Drawable        mBookmarkDrawable;
+    private View            mRtButton;
+    private View            mDivider;
+    private ProgressBar     mCircularProgress;
+    private ProgressBar     mHorizontalProgress;
+    private ImageView       mFavicon;
+    private ImageView       mLockIcon;
+    private boolean         mInLoad;
+    private boolean         mTitleSet;
+
+    public TitleBar(Context context) {
+        this(context, null);
+    }
+
+    public TitleBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        LayoutInflater factory = LayoutInflater.from(context);
+        factory.inflate(R.layout.title_bar, this);
+
+        mTitle = (TextView) findViewById(R.id.title);
+        mUrl = (TextView) findViewById(R.id.url);
+
+        mLftButton = (ImageView) findViewById(R.id.lft_button);
+        mRtButton = findViewById(R.id.rt_button);
+
+        mCircularProgress = (ProgressBar) findViewById(R.id.progress_circular);
+        mHorizontalProgress = (ProgressBar) findViewById(
+                R.id.progress_horizontal);
+        mFavicon = (ImageView) findViewById(R.id.favicon);
+        mLockIcon = (ImageView) findViewById(R.id.lock_icon);
+        mDivider = findViewById(R.id.divider);
+    }
+
+    /* package */ void setBrowserActivity(final BrowserActivity activity) {
+        mLftButton.setOnClickListener(new View.OnClickListener() {
+                    public void onClick(View v) {
+                        if (mInLoad) {
+                            WebView webView = activity.getTopWindow();
+                            if (webView != null) {
+                                webView.stopLoading();
+                            }
+                        } else {
+                            activity.bookmarksOrHistoryPicker(false);
+                        }
+                    }
+                });
+        mRtButton.setOnClickListener(new View.OnClickListener() {
+                    public void onClick(View v) {
+                        WebView webView = activity.getTopWindow();
+                        if (webView != null) {
+                            webView.zoomScrollOut();
+                        }
+                    }
+                });
+        setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                activity.onSearchRequested();
+            }
+        });
+    }
+
+    /* package */ void setFavicon(Drawable d) {
+        mFavicon.setImageDrawable(d);
+    }
+
+    /* package */ void setLock(Drawable d) {
+        if (d == null) {
+            mLockIcon.setVisibility(View.GONE);
+        } else {
+            mLockIcon.setImageDrawable(d);
+            mLockIcon.setVisibility(View.VISIBLE);
+        }
+    }
+
+    /* package */ void setProgress(int newProgress) {
+        if (newProgress == mCircularProgress.getMax()) {
+            mCircularProgress.setVisibility(View.GONE);
+            mHorizontalProgress.setVisibility(View.GONE);
+            mDivider.setVisibility(View.VISIBLE);
+            mRtButton.setVisibility(View.VISIBLE);
+            mLftButton.setImageDrawable(mBookmarkDrawable);
+            mInLoad = false;
+            if (!mTitleSet) {
+                mTitle.setText(mUrl.getText());
+                mUrl.setText(null);
+                mTitleSet = true;
+            }
+        } else {
+            mCircularProgress.setProgress(newProgress);
+            mHorizontalProgress.setProgress(newProgress);
+            mCircularProgress.setVisibility(View.VISIBLE);
+            mHorizontalProgress.setVisibility(View.VISIBLE);
+            mDivider.setVisibility(View.GONE);
+            mRtButton.setVisibility(View.GONE);
+            if (mBookmarkDrawable == null) {
+                // The drawable was assigned in the xml file, so it already
+                // exists.  Keep a pointer to it when we switch to the resource
+                // so we can easily switch back.
+                mBookmarkDrawable = mLftButton.getDrawable();
+            }
+            mLftButton.setImageResource(
+                    com.android.internal.R.drawable.ic_menu_stop);
+            mInLoad = true;
+        }
+    }
+
+    /* package */ void setTitleAndUrl(CharSequence title, CharSequence url) {
+        if (url != null) {
+            url = BrowserActivity.buildTitleUrl(url.toString());
+        }
+        if (null == title) {
+            if (mInLoad) {
+                mTitleSet = false;
+                mTitle.setText(R.string.title_bar_loading);
+            } else {
+                // If the page has no title, put the url in the title space
+                // and leave the url blank.
+                mTitle.setText(url);
+                mUrl.setText(null);
+                mTitleSet = true;
+                return;
+            }
+        } else {
+            mTitle.setText(title);
+            mTitleSet = true;
+        }
+        mUrl.setText(url);
+    }
+
+    /* package */ void setToTabPicker() {
+        mTitle.setText(R.string.tab_picker_title);
+        setFavicon(null);
+        setLock(null);
+        mCircularProgress.setVisibility(View.GONE);
+        mHorizontalProgress.setVisibility(View.GONE);
+    }
+}
diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java
new file mode 100644
index 0000000..40d30a2
--- /dev/null
+++ b/src/com/android/browser/WebStorageSizeManager.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2009 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.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.StatFs;
+import android.util.Log;
+import android.webkit.WebStorage;
+
+import java.io.File;
+import java.util.Set;
+
+
+/**
+ * Package level class for managing the disk size consumed by the WebDatabase
+ * and ApplicationCaches APIs (henceforth called Web storage).
+ *
+ * Currently, the situation on the WebKit side is as follows:
+ *  - WebDatabase enforces a quota for each origin.
+ *  - Session/LocalStorage do not enforce any disk limits.
+ *  - ApplicationCaches enforces a maximum size for all origins.
+ *
+ * The WebStorageSizeManager maintains a global limit for the disk space
+ * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
+ * have a limit for Session/LocalStorage, this class will manage the space used
+ * by those APIs as well.
+ *
+ * The global limit is computed as a function of the size of the partition where
+ * these APIs store their data (they must store it on the same partition for
+ * this to work) and the size of the available space on that partition.
+ * The global limit is not subject to user configuration but we do provide
+ * a debug-only setting.
+ * TODO(andreip): implement the debug setting.
+ *
+ * The size of the disk space used for Web storage is initially divided between
+ * WebDatabase and ApplicationCaches as follows:
+ *
+ * 75% for WebDatabase
+ * 25% for ApplicationCaches
+ *
+ * When an origin's database usage reaches its current quota, WebKit invokes
+ * the following callback function:
+ * - exceededDatabaseQuota(Frame* frame, const String& database_name);
+ * Note that the default quota for a new origin is 0, so we will receive the
+ * 'exceededDatabaseQuota' callback before a new origin gets the chance to
+ * create its first database.
+ *
+ * When the total ApplicationCaches usage reaches its current quota, WebKit
+ * invokes the following callback function:
+ * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
+ *
+ * The WebStorageSizeManager's main job is to respond to the above two callbacks
+ * by inspecting the amount of unused Web storage quota (i.e. global limit -
+ * sum of all other origins' quota) and deciding if a quota increase for the
+ * out-of-space origin is allowed or not.
+ *
+ * The default quota for an origin is min(ORIGIN_DEFAULT_QUOTA, unused_quota).
+ * Quota increases are done in steps, where the increase step is
+ * min(QUOTA_INCREASE_STEP, unused_quota).
+ *
+ * This approach has the drawback that space may remain unused if there
+ * are many websites that store a lot less content than ORIGIN_DEFAULT_QUOTA.
+ * We deal with this by picking a value for ORIGIN_DEFAULT_QUOTA that is smaller
+ * than what the HTML 5 spec recommends. At the same time, picking a very small
+ * value for ORIGIN_DEFAULT_QUOTA may create performance problems since it's
+ * more likely for origins to have to rollback and restart transactions as a
+ * result of reaching the quota more often.
+ *
+ * When all the Web storage space is used, the WebStorageSizeManager creates
+ * a system notification that will guide the user to the WebSettings UI. There,
+ * the user can free some of the Web storage space by deleting all the data used
+ * by an origin.
+ */
+class WebStorageSizeManager {
+    // Logging flags.
+    private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED;
+    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+    private final static String LOGTAG = "browser";
+    // The default quota value for an origin.
+    public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024;  // 3MB
+    // The default value for quota increases.
+    public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024;  // 1MB
+    // Extra padding space for appcache maximum size increases. This is needed
+    // because WebKit sends us an estimate of the amount of space needed
+    // but this estimate may, currently, be slightly less than what is actually
+    // needed. We therefore add some 'padding'.
+    // TODO(andreip): fix this in WebKit.
+    public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
+    // The system status bar notification id.
+    private final static int OUT_OF_SPACE_ID = 1;
+    // The application context.
+    private final Context mContext;
+    // The global Web storage limit.
+    private final long mGlobalLimit;
+    // The maximum size of the application cache file.
+    private long mAppCacheMaxSize;
+
+    /**
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the underlying file system. This functionality is separated
+     * into its own interface mainly for testing purposes.
+     */
+    public interface DiskInfo {
+        /**
+         * @return the size of the free space in the file system.
+         */
+        public long getFreeSpaceSizeBytes();
+
+        /**
+         * @return the total size of the file system.
+         */
+        public long getTotalSizeBytes();
+    };
+
+    private DiskInfo mDiskInfo;
+    // For convenience, we provide a DiskInfo implementation that uses StatFs.
+    public static class StatFsDiskInfo implements DiskInfo {
+        private StatFs mFs;
+
+        public StatFsDiskInfo(String path) {
+            mFs = new StatFs(path);
+        }
+
+        public long getFreeSpaceSizeBytes() {
+            return mFs.getAvailableBlocks() * mFs.getBlockSize();
+        }
+
+        public long getTotalSizeBytes() {
+            return mFs.getBlockCount() * mFs.getBlockSize();
+        }
+    };
+
+    /**
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the appcache file. This functionality is separated into its own
+     * interface mainly for testing purposes.
+     */
+    public interface AppCacheInfo {
+        /**
+         * @return the current size of the appcache file.
+         */
+        public long getAppCacheSizeBytes();
+    };
+
+    // For convenience, we provide an AppCacheInfo implementation.
+    public static class WebKitAppCacheInfo implements AppCacheInfo {
+        // The name of the application cache file. Keep in sync with
+        // WebCore/loader/appcache/ApplicationCacheStorage.cpp
+        private final static String APPCACHE_FILE = "ApplicationCache.db";
+        private String mAppCachePath;
+
+        public WebKitAppCacheInfo(String path) {
+            mAppCachePath = path;
+        }
+
+        public long getAppCacheSizeBytes() {
+            File file = new File(mAppCachePath
+                    + File.separator
+                    + APPCACHE_FILE);
+            return file.length();
+        }
+    };
+
+    /**
+     * Public ctor
+     * @param ctx is the application context
+     * @param diskInfo is the DiskInfo instance used to query the file system.
+     * @param appCacheInfo is the AppCacheInfo used to query info about the
+     * appcache file.
+     */
+    public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
+            AppCacheInfo appCacheInfo) {
+        mContext = ctx;
+        mDiskInfo = diskInfo;
+        mGlobalLimit = getGlobalLimit();
+        // The initial max size of the app cache is either 25% of the global
+        // limit or the current size of the app cache file, whichever is bigger.
+        mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
+                appCacheInfo.getAppCacheSizeBytes());
+    }
+
+    /**
+     * Returns the maximum size of the application cache.
+     */
+    public long getAppCacheMaxSize() {
+        return mAppCacheMaxSize;
+    }
+
+    /**
+     * The origin has exceeded its database quota.
+     * @param url the URL that exceeded the quota
+     * @param databaseIdentifier the identifier of the database on
+     *     which the transaction that caused the quota overflow was run
+     * @param currentQuota the current quota for the origin.
+     * @param totalUsedQuota is the sum of all origins' quota.
+     * @param quotaUpdater The callback to run when a decision to allow or
+     *     deny quota has been made. Don't forget to call this!
+     */
+    public void onExceededDatabaseQuota(String url,
+        String databaseIdentifier, long currentQuota, long totalUsedQuota,
+        WebStorage.QuotaUpdater quotaUpdater) {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG,
+                  "Received onExceededDatabaseQuota for "
+                  + url
+                  + ":"
+                  + databaseIdentifier
+                  + "(current quota: "
+                  + currentQuota
+                  + ", total used quota: "
+                  + totalUsedQuota
+                  + ")");
+        }
+        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+        if (totalUnusedQuota <= 0) {
+            // There definitely isn't any more space. Fire notifications
+            // if needed and exit.
+            if (totalUsedQuota > 0) {
+                // We only fire the notification if there are some other websites
+                // using some of the quota. This avoids the degenerate case where
+                // the first ever website to use Web storage tries to use more
+                // data than it is actually available. In such a case, showing
+                // the notification would not help at all since there is nothing
+                // the user can do.
+                scheduleOutOfSpaceNotification();
+            }
+            quotaUpdater.updateQuota(currentQuota);
+            if(LOGV_ENABLED) {
+                Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
+            }
+            return;
+        }
+        // We have enough space inside mGlobalLimit.
+        long newOriginQuota = currentQuota;
+        if (newOriginQuota == 0) {
+            // This is a new origin. It wants an initial quota.
+            newOriginQuota =
+                Math.min(ORIGIN_DEFAULT_QUOTA, totalUnusedQuota);
+        } else {
+            // This is an origin we have seen before. It wants a quota
+            // increase.
+            newOriginQuota +=
+                Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
+        }
+        quotaUpdater.updateQuota(newOriginQuota);
+
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
+                    + newOriginQuota);
+        }
+    }
+
+    /**
+     * The Application Cache has exceeded its max size.
+     * @param spaceNeeded is the amount of disk space that would be needed
+     * in order for the last appcache operation to succeed.
+     * @param totalUsedQuota is the sum of all origins' quota.
+     * @param quotaUpdater A callback to inform the WebCore thread that a new
+     * app cache size is available. This callback must always be executed at
+     * some point to ensure that the sleeping WebCore thread is woken up.
+     */
+    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
+            WebStorage.QuotaUpdater quotaUpdater) {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
+                  + spaceNeeded + " bytes.");
+        }
+
+        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+        if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
+            // There definitely isn't any more space. Fire notifications
+            // if needed and exit.
+            if (totalUsedQuota > 0) {
+                // We only fire the notification if there are some other websites
+                // using some of the quota. This avoids the degenerate case where
+                // the first ever website to use Web storage tries to use more
+                // data than it is actually available. In such a case, showing
+                // the notification would not help at all since there is nothing
+                // the user can do.
+                scheduleOutOfSpaceNotification();
+            }
+            quotaUpdater.updateQuota(0);
+            if(LOGV_ENABLED) {
+                Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
+            }
+            return;
+        }
+        // There is enough space to accommodate spaceNeeded bytes.
+        mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
+        quotaUpdater.updateQuota(mAppCacheMaxSize);
+
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
+                    + mAppCacheMaxSize);
+        }
+    }
+
+    // Computes the global limit as a function of the size of the data
+    // partition and the amount of free space on that partition.
+    private long getGlobalLimit() {
+        long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
+        long fileSystemSize = mDiskInfo.getTotalSizeBytes();
+        return calculateGlobalLimit(fileSystemSize, freeSpace);
+    }
+
+    /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
+            long freeSpaceBytes) {
+        if (fileSystemSizeBytes <= 0
+                || freeSpaceBytes <= 0
+                || freeSpaceBytes > fileSystemSizeBytes) {
+            return 0;
+        }
+
+        long fileSystemSizeRatio =
+            2 << ((int) Math.floor(Math.log10(
+                    fileSystemSizeBytes / (1024 * 1024))));
+        long maxSizeBytes = (long) Math.min(Math.floor(
+                fileSystemSizeBytes / fileSystemSizeRatio),
+                Math.floor(freeSpaceBytes / 2));
+        // Round maxSizeBytes up to a multiple of 1024KB (but only if
+        // maxSizeBytes > 1MB).
+        long maxSizeStepBytes = 1024 * 1024;
+        if (maxSizeBytes < maxSizeStepBytes) {
+            return 0;
+        }
+        long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
+        return (maxSizeStepBytes
+                * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
+    }
+
+    // Schedules a system notification that takes the user to the WebSettings
+    // activity when clicked.
+    private void scheduleOutOfSpaceNotification() {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
+        }
+        if (mContext == null) {
+            // mContext can be null if we're running unit tests.
+            return;
+        }
+        // setup the notification boilerplate.
+        int icon = R.drawable.ic_launcher_browser;
+        CharSequence title = mContext.getString(
+                R.string.webstorage_outofspace_notification_title);
+        CharSequence text = mContext.getString(
+                R.string.webstorage_outofspace_notification_text);
+        long when = System.currentTimeMillis();
+        Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
+        PendingIntent contentIntent =
+            PendingIntent.getActivity(mContext, 0, intent, 0);
+        Notification notification = new Notification(icon, title, when);
+        notification.setLatestEventInfo(mContext, title, text, contentIntent);
+        notification.flags |= Notification.FLAG_AUTO_CANCEL;
+        // Fire away.
+        String ns = Context.NOTIFICATION_SERVICE;
+        NotificationManager mgr =
+            (NotificationManager) mContext.getSystemService(ns);
+        if (mgr != null) {
+            mgr.notify(OUT_OF_SPACE_ID, notification);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/browser/WebsiteSettingsActivity.java b/src/com/android/browser/WebsiteSettingsActivity.java
new file mode 100644
index 0000000..89e5963
--- /dev/null
+++ b/src/com/android/browser/WebsiteSettingsActivity.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2009 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.AlertDialog;
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.GeolocationPermissions;
+import android.webkit.WebIconDatabase;
+import android.webkit.WebStorage;
+import android.widget.ArrayAdapter;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Vector;
+
+/**
+ * Manage the settings for an origin.
+ * We use it to keep track of the 'HTML5' settings, i.e. database (webstorage)
+ * and Geolocation.
+ */
+public class WebsiteSettingsActivity extends ListActivity {
+
+    private String LOGTAG = "WebsiteSettingsActivity";
+    private static String sMBStored = null;
+    private SiteAdapter mAdapter = null;
+
+    class Site {
+        private String mOrigin;
+        private String mTitle;
+        private Bitmap mIcon;
+        private int mFeatures;
+
+        // These constants provide the set of features that a site may support
+        // They must be consecutive. To add a new feature, add a new FEATURE_XXX
+        // variable with value equal to the current value of FEATURE_COUNT, then
+        // increment FEATURE_COUNT.
+        private final static int FEATURE_WEB_STORAGE = 0;
+        private final static int FEATURE_GEOLOCATION = 1;
+        // The number of features available.
+        private final static int FEATURE_COUNT = 2;
+
+        public Site(String origin) {
+            mOrigin = origin;
+            mTitle = null;
+            mIcon = null;
+            mFeatures = 0;
+        }
+
+        public void addFeature(int feature) {
+            mFeatures |= (1 << feature);
+        }
+
+        public boolean hasFeature(int feature) {
+            return (mFeatures & (1 << feature)) != 0;
+        }
+
+        /**
+         * Gets the number of features supported by this site.
+         */
+        public int getFeatureCount() {
+            int count = 0;
+            for (int i = 0; i < FEATURE_COUNT; ++i) {
+                count += hasFeature(i) ? 1 : 0;
+            }
+            return count;
+        }
+
+        /**
+         * Gets the ID of the nth (zero-based) feature supported by this site.
+         * The return value is a feature ID - one of the FEATURE_XXX values.
+         * This is required to determine which feature is displayed at a given
+         * position in the list of features for this site. This is used both
+         * when populating the view and when responding to clicks on the list.
+         */
+        public int getFeatureByIndex(int n) {
+            int j = -1;
+            for (int i = 0; i < FEATURE_COUNT; ++i) {
+                j += hasFeature(i) ? 1 : 0;
+                if (j == n) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public String getOrigin() {
+            return mOrigin;
+        }
+
+        public void setTitle(String title) {
+            mTitle = title;
+        }
+
+        public void setIcon(Bitmap icon) {
+            mIcon = icon;
+        }
+
+        public Bitmap getIcon() {
+            return mIcon;
+        }
+
+        public String getPrettyOrigin() {
+            return mTitle == null ? null : hideHttp(mOrigin);
+        }
+
+        public String getPrettyTitle() {
+            return mTitle == null ? hideHttp(mOrigin) : mTitle;
+        }
+
+        private String hideHttp(String str) {
+            Uri uri = Uri.parse(str);
+            return "http".equals(uri.getScheme()) ?  str.substring(7) : str;
+        }
+    }
+
+    class SiteAdapter extends ArrayAdapter<Site>
+            implements AdapterView.OnItemClickListener {
+        private int mResource;
+        private LayoutInflater mInflater;
+        private Bitmap mDefaultIcon;
+        private Site mCurrentSite;
+
+        public SiteAdapter(Context context, int rsc) {
+            super(context, rsc);
+            mResource = rsc;
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mDefaultIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_launcher_shortcut_browser_bookmark);
+            populateOrigins();
+        }
+
+        /**
+         * Adds the specified feature to the site corresponding to supplied
+         * origin in the map. Creates the site if it does not already exist.
+         */
+        private void addFeatureToSite(Map sites, String origin, int feature) {
+            Site site = null;
+            if (sites.containsKey(origin)) {
+                site = (Site) sites.get(origin);
+            } else {
+                site = new Site(origin);
+                sites.put(origin, site);
+            }
+            site.addFeature(feature);
+        }
+
+        public void populateOrigins() {
+            clear();
+
+            // Get the list of origins we want to display.
+            // All 'HTML 5 modules' (Database, Geolocation etc) form these
+            // origin strings using WebCore::SecurityOrigin::toString(), so it's
+            // safe to group origins here. Note that WebCore::SecurityOrigin
+            // uses 0 (which is not printed) for the port if the port is the
+            // default for the protocol. Eg http://www.google.com and
+            // http://www.google.com:80 both record a port of 0 and hence
+            // toString() == 'http://www.google.com' for both.
+            Set origins = WebStorage.getInstance().getOrigins();
+            Map sites = new HashMap<String, Site>();
+            if (origins != null) {
+                Iterator<String> iter = origins.iterator();
+                while (iter.hasNext()) {
+                    addFeatureToSite(sites, iter.next(), Site.FEATURE_WEB_STORAGE);
+                }
+            }
+            origins = GeolocationPermissions.getInstance().getOrigins();
+            if (origins != null) {
+                Iterator<String> iter = origins.iterator();
+                while (iter.hasNext()) {
+                    addFeatureToSite(sites, iter.next(), Site.FEATURE_GEOLOCATION);
+                }
+            }
+
+            // Create a map from host to origin. This is used to add metadata
+            // (title, icon) for this origin from the bookmarks DB.
+            HashMap hosts = new HashMap<String, Set<Site> >();
+            Set keys = sites.keySet();
+            Iterator<String> originIter = keys.iterator();
+            while (originIter.hasNext()) {
+                String origin = originIter.next();
+                Site site = (Site) sites.get(origin);
+                String host = Uri.parse(origin).getHost();
+                Set hostSites = null;
+                if (hosts.containsKey(host)) {
+                    hostSites = (Set) hosts.get(host);
+                } else {
+                    hostSites = new HashSet<Site>();
+                    hosts.put(host, hostSites);
+                }
+                hostSites.add(site);
+            }
+
+            // Check the bookmark DB. If we have data for a host used by any of
+            // our origins, use it to set their title and favicon
+            Cursor c = getContext().getContentResolver().query(Browser.BOOKMARKS_URI,
+                    new String[] { Browser.BookmarkColumns.URL, Browser.BookmarkColumns.TITLE,
+                    Browser.BookmarkColumns.FAVICON }, "bookmark = 1", null, null);
+
+            if ((c != null) && c.moveToFirst()) {
+                int urlIndex = c.getColumnIndex(Browser.BookmarkColumns.URL);
+                int titleIndex = c.getColumnIndex(Browser.BookmarkColumns.TITLE);
+                int faviconIndex = c.getColumnIndex(Browser.BookmarkColumns.FAVICON);
+                do {
+                    String url = c.getString(urlIndex);
+                    String host = Uri.parse(url).getHost();
+                    if (hosts.containsKey(host)) {
+                        String title = c.getString(titleIndex);
+                        Bitmap bmp = null;
+                        byte[] data = c.getBlob(faviconIndex);
+                        if (data != null) {
+                            bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
+                        }
+                        Set matchingSites = (Set) hosts.get(host);
+                        Iterator<Site> sitesIter = matchingSites.iterator();
+                        while (sitesIter.hasNext()) {
+                            Site site = sitesIter.next();
+                            site.setTitle(title);
+                            if (bmp != null) {
+                                site.setIcon(bmp);
+                            }
+                        }
+                    }
+                } while (c.moveToNext());
+            }
+
+            // We can now simply populate our array with Site instances
+            keys = sites.keySet();
+            originIter = keys.iterator();
+            while (originIter.hasNext()) {
+                String origin = originIter.next();
+                Site site = (Site) sites.get(origin);
+                add(site);
+            }
+
+            if (getCount() == 0) {
+                finish(); // we close the screen
+            }
+        }
+
+        public int getCount() {
+            if (mCurrentSite == null) {
+                return super.getCount();
+            }
+            return mCurrentSite.getFeatureCount();
+        }
+
+        public String sizeValueToString(long value) {
+            float mb = (float) value / (1024.0F * 1024.0F);
+            int val = (int) (mb * 10);
+            float ret = (float) (val / 10.0F);
+            if (ret <= 0) {
+                return "0";
+            }
+            return String.valueOf(ret);
+        }
+
+        /*
+         * If we receive the back event and are displaying
+         * site's settings, we want to go back to the main
+         * list view. If not, we just do nothing (see
+         * dispatchKeyEvent() below).
+         */
+        public boolean backKeyPressed() {
+            if (mCurrentSite != null) {
+                mCurrentSite = null;
+                populateOrigins();
+                notifyDataSetChanged();
+                return true;
+            }
+            return false;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view;
+            TextView title;
+            TextView subtitle;
+            ImageView icon;
+
+            if (convertView == null) {
+                view = mInflater.inflate(mResource, parent, false);
+            } else {
+                view = convertView;
+            }
+
+            title = (TextView) view.findViewById(R.id.title);
+            subtitle = (TextView) view.findViewById(R.id.subtitle);
+            icon = (ImageView) view.findViewById(R.id.icon);
+
+            if (mCurrentSite == null) {
+                Site site = getItem(position);
+                title.setText(site.getPrettyTitle());
+                subtitle.setText(site.getPrettyOrigin());
+                icon.setVisibility(View.VISIBLE);
+                Bitmap bmp = site.getIcon();
+                if (bmp == null) {
+                    bmp = mDefaultIcon;
+                }
+                icon.setImageBitmap(bmp);
+                // We set the site as the view's tag,
+                // so that we can get it in onItemClick()
+                view.setTag(site);
+            } else {
+                icon.setVisibility(View.GONE);
+                String origin = mCurrentSite.getOrigin();
+                switch (mCurrentSite.getFeatureByIndex(position)) {
+                    case Site.FEATURE_WEB_STORAGE:
+                        long usageValue = WebStorage.getInstance().getUsageForOrigin(origin);
+                        String usage = sizeValueToString(usageValue) + " " + sMBStored;
+
+                        title.setText(R.string.webstorage_clear_data_title);
+                        subtitle.setText(usage);
+                        break;
+                    case Site.FEATURE_GEOLOCATION:
+                        title.setText(R.string.geolocation_settings_page_title);
+                        boolean allowed = GeolocationPermissions.getInstance().getAllowed(origin);
+                        subtitle.setText(allowed ?
+                                         R.string.geolocation_settings_page_summary_allowed :
+                                         R.string.geolocation_settings_page_summary_not_allowed);
+                        break;
+                }
+            }
+
+            return view;
+        }
+
+        public void onItemClick(AdapterView<?> parent,
+                                View view,
+                                int position,
+                                long id) {
+            if (mCurrentSite != null) {
+                switch (mCurrentSite.getFeatureByIndex(position)) {
+                    case Site.FEATURE_WEB_STORAGE:
+                        new AlertDialog.Builder(getContext())
+                            .setTitle(R.string.webstorage_clear_data_dialog_title)
+                            .setMessage(R.string.webstorage_clear_data_dialog_message)
+                            .setPositiveButton(R.string.webstorage_clear_data_dialog_ok_button,
+                                               new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dlg, int which) {
+                                    WebStorage.getInstance().deleteOrigin(mCurrentSite.getOrigin());
+                                    mCurrentSite = null;
+                                    populateOrigins();
+                                    notifyDataSetChanged();
+                                }})
+                            .setNegativeButton(R.string.webstorage_clear_data_dialog_cancel_button, null)
+                            .setIcon(android.R.drawable.ic_dialog_alert)
+                            .show();
+                        break;
+                    case Site.FEATURE_GEOLOCATION:
+                        new AlertDialog.Builder(getContext())
+                            .setTitle(R.string.geolocation_settings_page_dialog_title)
+                            .setMessage(R.string.geolocation_settings_page_dialog_message)
+                            .setPositiveButton(R.string.geolocation_settings_page_dialog_ok_button,
+                                               new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dlg, int which) {
+                                    GeolocationPermissions.getInstance().clear(mCurrentSite.getOrigin());
+                                    mCurrentSite = null;
+                                    populateOrigins();
+                                    notifyDataSetChanged();
+                                }})
+                            .setNegativeButton(R.string.geolocation_settings_page_dialog_cancel_button, null)
+                            .setIcon(android.R.drawable.ic_dialog_alert)
+                            .show();
+                        break;
+                }
+            } else {
+                mCurrentSite = (Site) view.getTag();
+                notifyDataSetChanged();
+            }
+        }
+    }
+
+    /**
+     * Intercepts the back key to immediately notify
+     * NativeDialog that we are done.
+     */
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        if ((event.getKeyCode() == KeyEvent.KEYCODE_BACK)
+            && (event.getAction() == KeyEvent.ACTION_DOWN)) {
+            if ((mAdapter != null) && (mAdapter.backKeyPressed())){
+                return true; // event consumed
+            }
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        if (sMBStored == null) {
+            sMBStored = getString(R.string.webstorage_origin_summary_mb_stored);
+        }
+        mAdapter = new SiteAdapter(this, R.layout.application);
+        setListAdapter(mAdapter);
+        getListView().setOnItemClickListener(mAdapter);
+    }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..f86942d
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,37 @@
+# Copyright 2008, 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Include all test java files.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+# Notice that we don't have to include the src files of Browser because, by
+# running the tests using an instrumentation targeting Browser, we
+# automatically get all of its classes loaded into our environment.
+
+LOCAL_PACKAGE_NAME := BrowserTests
+
+LOCAL_INSTRUMENTATION_FOR := Browser
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
+
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..51715a9
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2008 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 name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.browser.tests">
+
+    <!-- We add an application tag here just so that we can indicate that
+         this package needs to link against the android.test library,
+         which is needed when building test cases. -->
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!--
+    This declares that this app uses the instrumentation test runner targeting
+    the package of com.android.email.  To run the tests use the command:
+    "adb shell am instrument -w com.android.browser.tests/android.test.InstrumentationTestRunner"
+    -->
+    <instrumentation android:name="android.test.InstrumentationTestRunner"
+                     android:targetPackage="com.android.browser"
+                     android:label="Tests for Browser."/>
+
+</manifest>
diff --git a/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
new file mode 100644
index 0000000..a2604e0
--- /dev/null
+++ b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2009 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.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.webkit.WebStorage;
+
+/**
+ * This is a series of unit tests for the WebStorageSizeManager class.
+ *
+ */
+@MediumTest
+public class WebStorageSizeManagerUnitTests extends AndroidTestCase {
+    // Used for testing the out-of-space callbacks.
+    private long mNewQuota;
+    // Callback functor that sets a new quota in case of out-of-space scenarios.
+    private class MockQuotaUpdater implements WebStorage.QuotaUpdater {
+        public void updateQuota(long newQuota) {
+            mNewQuota = newQuota;
+        }
+    }
+
+    // Mock the DiskInfo.
+    private class MockDiskInfo implements WebStorageSizeManager.DiskInfo {
+        private long mFreeSize;
+        private long mTotalSize;
+
+        public long getFreeSpaceSizeBytes() {
+            return mFreeSize;
+        }
+
+        public long getTotalSizeBytes() {
+            return mTotalSize;
+        }
+
+        public void setFreeSpaceSizeBytes(long freeSize) {
+            mFreeSize = freeSize;
+        }
+
+        public void setTotalSizeBytes(long totalSize) {
+            mTotalSize = totalSize;
+        }
+    }
+
+    // Mock the AppCacheInfo
+    public class MockAppCacheInfo implements WebStorageSizeManager.AppCacheInfo {
+        private long mAppCacheSize;
+
+        public long getAppCacheSizeBytes() {
+            return mAppCacheSize;
+        }
+
+        public void setAppCacheSizeBytes(long appCacheSize) {
+            mAppCacheSize = appCacheSize;
+        }
+    }
+
+    private MockQuotaUpdater mQuotaUpdater = new MockQuotaUpdater();
+    private final MockDiskInfo mDiskInfo = new MockDiskInfo();
+    private final MockAppCacheInfo mAppCacheInfo = new MockAppCacheInfo();
+    // Utility for making size computations easier to read.
+    private long bytes(double megabytes) {
+        return (new Double(megabytes * 1024 * 1024)).longValue();
+    }
+    /**
+     * Test the onExceededDatabaseQuota and onReachedMaxAppCacheSize callbacks
+     */
+    public void testCallbacks() {
+        long totalUsedQuota = 0;
+        final long defaultQuota = WebStorageSizeManager.ORIGIN_DEFAULT_QUOTA;  // 3MB
+        final long quotaIncrease = WebStorageSizeManager.QUOTA_INCREASE_STEP;  // 1MB
+        // We have 75 MB total, 24MB free so the global limit will be 12 MB.
+        mDiskInfo.setTotalSizeBytes(bytes(75));
+        mDiskInfo.setFreeSpaceSizeBytes(bytes(24));
+        // We have an appcache file size of 0 MB.
+        mAppCacheInfo.setAppCacheSizeBytes(0);
+        // Create the manager.
+        WebStorageSizeManager manager = new WebStorageSizeManager(null, mDiskInfo, mAppCacheInfo);
+        // We add origin 1.
+        long origin1Quota = 0;
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota, mNewQuota);
+        origin1Quota = mNewQuota;
+        totalUsedQuota += origin1Quota;
+
+        // We add origin 2.
+        long origin2Quota = 0;
+        manager.onExceededDatabaseQuota("2", "2", origin2Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota, mNewQuota);
+        origin2Quota = mNewQuota;
+        totalUsedQuota += origin2Quota;
+
+        // Origin 1 runs out of space.
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota + quotaIncrease, mNewQuota);
+        totalUsedQuota -= origin1Quota;
+        origin1Quota = mNewQuota;
+        totalUsedQuota += origin1Quota;
+
+        // Origin 2 runs out of space.
+        manager.onExceededDatabaseQuota("2", "2", origin2Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota + quotaIncrease, mNewQuota);
+        totalUsedQuota -= origin2Quota;
+        origin2Quota = mNewQuota;
+        totalUsedQuota += origin2Quota;
+
+        // We add origin 3. TotalUsedQuota is 8 (3 + 3 + 1 + 1). AppCacheMaxSize is 3 (12 / 4).
+        // So we have 1 MB free.
+        long origin3Quota = 0;
+        manager.onExceededDatabaseQuota("3", "3", origin3Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(bytes(1), mNewQuota);  // 1MB
+        origin3Quota = mNewQuota;
+        totalUsedQuota += origin3Quota;
+
+        // Origin 1 runs out of space again. We're also out of space so we can't give it more.
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(origin1Quota, mNewQuota);
+
+        // We try adding a new origin. Which will fail.
+        manager.onExceededDatabaseQuota("4", "4", 0, totalUsedQuota, mQuotaUpdater);
+        assertEquals(0, mNewQuota);
+
+        // AppCache size increases to 2MB...
+        mAppCacheInfo.setAppCacheSizeBytes(bytes(2));
+        // ... and wants 2MB more. Fail.
+        manager.onReachedMaxAppCacheSize(bytes(2), totalUsedQuota, mQuotaUpdater);
+        assertEquals(0, mNewQuota);
+
+        // The user nukes origin 2
+        totalUsedQuota -= origin2Quota;
+        origin2Quota = 0;
+        // TotalUsedQuota is 5 (9 - 4). AppCacheMaxSize is 3. AppCacheSize is 2.
+        // AppCache wants 1.5MB more
+        manager.onReachedMaxAppCacheSize(bytes(1.5), totalUsedQuota, mQuotaUpdater);
+        mAppCacheInfo.setAppCacheSizeBytes(mAppCacheInfo.getAppCacheSizeBytes() + bytes(2.5));
+        assertEquals(mAppCacheInfo.getAppCacheSizeBytes(), mNewQuota);
+
+        // We try adding a new origin. This time we succeed.
+        // TotalUsedQuota is 5. AppCacheMaxSize is 4.5. So we have 12 - 9.5 = 2.5 available.
+        long origin4Quota = 0;
+        manager.onExceededDatabaseQuota("4", "4", origin4Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(bytes(2.5), mNewQuota);
+        origin4Quota = mNewQuota;
+        totalUsedQuota += origin4Quota;
+    }
+    /**
+     * Test the application caches max size calculator.
+     */
+    public void testCalculateGlobalLimit() {
+        long fileSystemSize = 78643200;  // 75 MB
+        long freeSpaceSize = 25165824;  // 24 MB
+        long maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(12582912, maxSize);  // 12MB
+
+        fileSystemSize = 78643200;  // 75 MB
+        freeSpaceSize = 60 * 1024 * 1024;  // 60MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(19922944, maxSize);  // 19MB
+
+        fileSystemSize = 8589934592L;  // 8 GB
+        freeSpaceSize = 4294967296L;  // 4 GB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(536870912L, maxSize);  // 512 MB
+
+        fileSystemSize = -14;
+        freeSpaceSize = 21;
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 100;
+        freeSpaceSize = 101;
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~4.2 MB
+        freeSpaceSize = 2560000;  // ~2.4 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
+
+        fileSystemSize = 4404019; // ~4.2 MB
+        freeSpaceSize = 3774873;  // ~3.6 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
+
+        fileSystemSize = 4404019; // ~4.2 MB
+        freeSpaceSize = 4404019;  // ~4.2 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(3145728, maxSize);  // 3 MB
+
+        fileSystemSize = 1048576; // 1 MB
+        freeSpaceSize = 1048575;  // 1 MB - 1 byte
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~3.6 MB
+        freeSpaceSize = 2097151;  // 2 MB - 1 byte
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~3.6 MB
+        freeSpaceSize = 2097151;  // 2 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+    }
+}