Merge "Delay computing of PhoneNumber as much as possible."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d2cc492..61f0960 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -41,6 +41,8 @@
     <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
     <uses-permission android:name="com.android.voicemail.permission.READ_WRITE_OWN_VOICEMAIL" />
     <uses-permission android:name="com.android.voicemail.permission.READ_WRITE_ALL_VOICEMAIL" />
+    <!-- allow broadcasting secret code intents that reboot the phone -->
+    <uses-permission android:name="android.permission.REBOOT" />
 
     <application
         android:name="com.android.contacts.ContactsApplication"
@@ -255,7 +257,7 @@
                 android:resource="@xml/searchable"
             />
         </activity>
-        
+
         <activity android:name=".activities.ContactSelectionActivity"
             android:label="@string/contactsList"
             android:theme="@style/ContactPickerTheme"
diff --git a/res/layout-sw580dp-w1000dp/group_detail_fragment.xml b/res/layout-sw580dp-w1000dp/group_detail_fragment.xml
new file mode 100644
index 0000000..11779cb
--- /dev/null
+++ b/res/layout-sw580dp-w1000dp/group_detail_fragment.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/group_detail"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/panel_content">
+
+    <!-- Static header containing the group title, size, and group source (if applicable) -->
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingBottom="@dimen/group_detail_vertical_padding"
+        android:orientation="horizontal" >
+
+        <LinearLayout
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical" >
+
+            <TextView
+                android:id="@+id/group_title"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/group_detail_border_padding"
+                android:paddingTop="@dimen/group_detail_vertical_padding"
+                android:textAppearance="?android:attr/textAppearanceLarge"
+                android:textStyle="bold" />
+
+            <TextView
+                android:id="@+id/group_size"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/group_detail_border_padding"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:textColor="?android:attr/textColorSecondary" />
+
+        </LinearLayout>
+
+        <FrameLayout
+            android:id="@+id/group_source_view_container"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="center_vertical"
+            android:paddingRight="@dimen/group_detail_border_padding" />
+
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:background="?android:attr/listDivider"
+        android:layout_marginBottom="@dimen/group_detail_vertical_padding"
+        android:layout_marginLeft="@dimen/group_detail_divider_margin"
+        android:layout_marginRight="@dimen/group_detail_divider_margin" />
+
+    <!-- List of group members -->
+    <ListView android:id="@android:id/list"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginLeft="@dimen/group_detail_border_padding"
+        android:layout_marginRight="@dimen/group_detail_border_padding"
+        android:cacheColorHint="#00000000"
+        android:divider="@null" />
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw580dp/group_detail_fragment.xml b/res/layout-sw580dp/group_detail_fragment.xml
index 7c65036..a7db154 100644
--- a/res/layout-sw580dp/group_detail_fragment.xml
+++ b/res/layout-sw580dp/group_detail_fragment.xml
@@ -49,6 +49,13 @@
         android:layout_marginLeft="@dimen/group_detail_divider_margin"
         android:layout_marginRight="@dimen/group_detail_divider_margin" />
 
+    <FrameLayout
+        android:id="@+id/group_source_view_container"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:paddingLeft="@dimen/group_detail_border_padding" />
+
     <ListView android:id="@android:id/list"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/res/layout-sw580dp/group_source_button.xml b/res/layout-sw580dp/group_source_button.xml
new file mode 100644
index 0000000..6b0b8fe
--- /dev/null
+++ b/res/layout-sw580dp/group_source_button.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!--
+  Layout for the button that will launch the user into the source application
+  that the group came from (on a group detail page).
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/group_source"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:padding="10dip" >
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:duplicateParentState="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/view_updates_from_group"/>
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:duplicateParentState="true"
+        android:layout_width="30dip"
+        android:layout_height="30dip"
+        android:layout_marginLeft="7dip"
+        android:layout_gravity="center_vertical"/>
+
+</LinearLayout>
diff --git a/res/layout-w470dp/group_source_button.xml b/res/layout-w470dp/group_source_button.xml
new file mode 100644
index 0000000..b725512
--- /dev/null
+++ b/res/layout-w470dp/group_source_button.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!--
+  Layout for the button that will launch the user into the source application
+  that the group came from (on a group detail page). This will be used in the
+  action bar, so it has a vertical divider.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:divider="?android:attr/dividerVertical"
+    android:showDividers="end"
+    android:dividerPadding="12dip"
+    android:orientation="horizontal">
+
+    <LinearLayout
+        style="?android:attr/actionButtonStyle"
+        android:id="@+id/group_source"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:paddingRight="5dip"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:text="@string/view_updates_from_group"/>
+
+        <ImageView
+            android:id="@android:id/icon"
+            android:layout_width="25dip"
+            android:layout_height="25dip"
+            android:layout_marginLeft="7dip"
+            android:layout_marginRight="7dip"
+            android:layout_centerVertical="true"/>
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/call_detail.xml b/res/layout/call_detail.xml
index 1e40964..c20df49 100644
--- a/res/layout/call_detail.xml
+++ b/res/layout/call_detail.xml
@@ -36,12 +36,24 @@
             android:src="@drawable/ic_call_log_home"
         />
     </LinearLayout>
+
+    <FrameLayout
+        android:id="@+id/voicemail_status"
+        android:layout_below="@id/action_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:visibility="gone"
+    >
+        <include layout="@layout/call_log_voicemail_status"/>
+    </FrameLayout>
+
     <ImageView
         android:id="@+id/contact_background"
         android:layout_width="match_parent"
         android:layout_height="?attr/call_detail_contact_background_height"
         android:layout_alignParentLeft="true"
-        android:layout_below="@id/action_bar"
+        android:layout_below="@id/voicemail_status"
         android:adjustViewBounds="true"
         android:scaleType="centerCrop"
         android:background="@drawable/ic_contact_picture"
diff --git a/res/layout/call_log_fragment.xml b/res/layout/call_log_fragment.xml
index 573969b..2d0b9b5 100644
--- a/res/layout/call_log_fragment.xml
+++ b/res/layout/call_log_fragment.xml
@@ -19,7 +19,16 @@
     android:layout_height="match_parent"
     android:orientation="vertical"
 >
-    <include layout="@layout/call_log_voicemail_status"/>
+    <FrameLayout
+        android:id="@+id/voicemail_status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentLeft="true"
+        android:layout_alignParentBottom="true"
+        android:visibility="gone">
+        <include layout="@layout/call_log_voicemail_status"
+    />
+    </FrameLayout>
     <FrameLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -29,7 +38,6 @@
             android:layout_height="match_parent"
             android:scrollbarStyle="outsideOverlay"
         />
-
         <TextView android:id="@android:id/empty"
             android:layout_width="match_parent"
             android:layout_height="match_parent"
@@ -39,4 +47,3 @@
         />
     </FrameLayout>
 </LinearLayout>
-
diff --git a/res/layout/call_log_voicemail_status.xml b/res/layout/call_log_voicemail_status.xml
index fee2210..c7def4d 100644
--- a/res/layout/call_log_voicemail_status.xml
+++ b/res/layout/call_log_voicemail_status.xml
@@ -13,41 +13,40 @@
      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/voicemail_status"
-    android:layout_width="match_parent"
-    android:layout_height="?attr/call_log_voicemail_status_height"
-    android:layout_alignParentLeft="true"
-    android:layout_alignParentBottom="true"
-    android:background="?attr/call_log_voicemail_status_background_color"
-    android:baselineAligned="false"
-    android:visibility="gone">
-    <TextView
-        android:id="@+id/voicemail_status_message"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:paddingLeft="14dip"
-        android:paddingRight="14dip"
-        android:textColor="?attr/call_log_voicemail_status_text_color"
-        android:layout_gravity="left"
-        android:layout_weight="5"/>
-    <View android:id="@+id/divider"
-        android:layout_width="1px"
-        android:layout_height="wrap_content"
-        android:layout_marginTop="5dip"
-        android:layout_marginBottom="5dip"
-        android:layout_marginLeft="11dip"
-        android:background="@drawable/divider_vertical_dark"/>
-    <TextView
-        android:id="@+id/voicemail_status_action"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="right"
-        android:paddingLeft="14dip"
-        android:paddingRight="14dip"
-        android:textColor="?attr/call_log_voicemail_status_text_color"
-        android:gravity="right"
-        android:layout_alignParentRight="true"
-        android:clickable="true"
-    />
-</LinearLayout>
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="?attr/call_log_voicemail_status_height"
+        android:background="?attr/call_log_voicemail_status_background_color">
+        <TextView
+            android:id="@+id/voicemail_status_message"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:paddingLeft="14dip"
+            android:paddingRight="14dip"
+            android:textColor="?attr/call_log_voicemail_status_text_color"
+            android:layout_gravity="left"
+            android:layout_weight="5"
+        />
+        <View android:id="@+id/divider"
+            android:layout_width="1px"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="5dip"
+            android:layout_marginBottom="5dip"
+            android:layout_marginLeft="11dip"
+            android:background="@drawable/divider_vertical_dark"
+        />
+        <TextView
+            android:id="@+id/voicemail_status_action"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:layout_gravity="right"
+            android:paddingLeft="14dip"
+            android:paddingRight="14dip"
+            android:textColor="?attr/call_log_voicemail_status_text_color"
+            android:gravity="right"
+            android:layout_alignParentRight="true"
+            android:clickable="true"
+        />
+    </LinearLayout>
+</merge>
diff --git a/res/layout/group_editor_fragment.xml b/res/layout/group_editor_fragment.xml
index dc82642..42321f4 100644
--- a/res/layout/group_editor_fragment.xml
+++ b/res/layout/group_editor_fragment.xml
@@ -48,6 +48,7 @@
         android:layout_height="wrap_content"
         android:textAppearance="?android:attr/textAppearanceLarge"
         android:inputType="textCapWords"
+        android:hint="@string/group_name_hint"
         android:layout_marginBottom="@dimen/group_editor_padding"/>
 
     <include
diff --git a/res/layout/group_editor_header.xml b/res/layout/group_editor_header.xml
index c6388fe..9b8bf88 100644
--- a/res/layout/group_editor_header.xml
+++ b/res/layout/group_editor_header.xml
@@ -38,6 +38,7 @@
         android:id="@+id/group_name"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        android:hint="@string/group_name_hint"
         android:textAppearance="?android:attr/textAppearanceLarge"
         android:inputType="textCapWords"/>
 
diff --git a/res/layout/group_list_header_item.xml b/res/layout/group_list_header_item.xml
new file mode 100644
index 0000000..b38920f
--- /dev/null
+++ b/res/layout/group_list_header_item.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:padding="@dimen/group_list_header_padding"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:id="@+id/account_type"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAppearance="?android:attr/textAppearanceMedium"
+            android:textColor="@color/people_app_theme_color"
+            android:textStyle="bold"
+            android:singleLine="true"/>
+
+        <TextView
+            android:id="@+id/account_name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="@dimen/group_list_header_padding"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:singleLine="true"
+            android:textColor="@color/people_app_theme_color"/>
+
+        <TextView
+            android:id="@+id/group_count"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:gravity="right"
+            android:singleLine="true"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorTertiary"/>
+
+    </LinearLayout>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:layout_marginTop="@dimen/group_list_header_padding"
+        android:background="@color/people_app_theme_color"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/group_source_button.xml b/res/layout/group_source_button.xml
new file mode 100644
index 0000000..0aa9c58
--- /dev/null
+++ b/res/layout/group_source_button.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<!--
+  Layout for the button that will launch the user into the source application
+  that the group came from (on a group detail page).
+-->
+
+<LinearLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/group_source"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:padding="10dip" >
+
+    <TextView
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:layout_gravity="center_vertical"
+        android:duplicateParentState="true"
+        android:textAppearance="?android:attr/textAppearanceMedium"
+        android:text="@string/view_updates_from_group"/>
+
+    <ImageView
+        android:id="@android:id/icon"
+        android:layout_width="30dip"
+        android:layout_height="30dip"
+        android:duplicateParentState="true"
+        android:layout_marginLeft="7dip"
+        android:layout_marginRight="7dip"
+        android:layout_centerVertical="true"/>
+
+</LinearLayout>
diff --git a/res/menu-sw580dp-w720dp/view_group.xml b/res/menu-sw580dp-w720dp/view_group.xml
index 8b0867b..8f7b7d6 100644
--- a/res/menu-sw580dp-w720dp/view_group.xml
+++ b/res/menu-sw580dp-w720dp/view_group.xml
@@ -23,11 +23,6 @@
         android:showAsAction="always" />
 
     <item
-        android:id="@+id/menu_rename_group"
-        android:icon="@drawable/ic_menu_settings_holo_light"
-        android:title="@string/menu_renameGroup" />
-
-    <item
         android:id="@+id/menu_delete_group"
         android:icon="@drawable/ic_menu_trash_holo_light"
         android:title="@string/menu_deleteGroup" />
diff --git a/res/menu/group_source.xml b/res/menu/group_source.xml
new file mode 100644
index 0000000..6d1af2d
--- /dev/null
+++ b/res/menu/group_source.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_group_source"
+        android:showAsAction="always" />
+</menu>
diff --git a/res/values-sw580dp/donottranslate_config.xml b/res/values-sw580dp/donottranslate_config.xml
index 9841fd5..5c59f16 100644
--- a/res/values-sw580dp/donottranslate_config.xml
+++ b/res/values-sw580dp/donottranslate_config.xml
@@ -21,4 +21,5 @@
     <bool name="config_use_two_panes">true</bool>
     <bool name="always_show_search_view">true</bool>
     <bool name="show_home_icon">true</bool>
+    <bool name="config_show_group_action_in_action_bar">false</bool>
 </resources>
diff --git a/res/values-w470dp/donottranslate_config.xml b/res/values-w470dp/donottranslate_config.xml
new file mode 100644
index 0000000..462015a
--- /dev/null
+++ b/res/values-w470dp/donottranslate_config.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2011, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<resources>
+    <bool name="config_show_group_action_in_action_bar">true</bool>
+</resources>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index c676f2d..7f4e237 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -60,6 +60,7 @@
     <color name="call_log_missed_call_highlight_color">#FF0000</color>
 
     <!-- Color of the text describing an unconsumed voicemail. -->
+
     <color name="call_log_voicemail_highlight_color">#33b5e5</color>
 
     <!-- Colour background for the voicemail playback ui. -->
@@ -73,4 +74,7 @@
 
     <!-- Colour of text that appears on the voicemail ui. -->
     <color name="voicemail_playback_ui_text">#cc696969</color>
+
+    <!-- Color of the theme of the People app -->
+    <color name="people_app_theme_color">#33B5E5</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 3bc1bc6..0d868e9 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -155,6 +155,9 @@
     <!-- Height of list sections (A, B, C) that show the first character of the contacts -->
     <dimen name="list_section_height">25dip</dimen>
 
+    <!-- Border padding for the group list header for each account -->
+    <dimen name="group_list_header_padding">5dip</dimen>
+
     <!-- Border padding for the group detail fragment header -->
     <dimen name="group_detail_border_padding">20dip</dimen>
 
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index 3447392..0c719c4 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -110,4 +110,10 @@
 
     <!-- If true, the "home" icon on the action bar will be shown. -->
     <bool name="show_home_icon">false</bool>
+
+    <!--
+      If true, the "view updates from group" button in the action bar will be
+      shown. Otherwise it will be part of the content on the group detail page.
+    -->
+    <bool name="config_show_group_action_in_action_bar">false</bool>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 209aab7..ab1f86f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1614,6 +1614,9 @@
     <!-- The title of the activity that edits an existing group [CHAR LIMIT=NONE] -->
     <string name="editGroup_title_edit">Edit group</string>
 
+    <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=20] -->
+    <string name="view_updates_from_group">View updates</string>
+
     <!-- Title of the notification of new voicemail. -->
     <string name="notification_voicemail_title">New voicemail</string>
 
@@ -1653,4 +1656,7 @@
 
     <!-- The counter for calls in a group in the call log [CHAR LIMIT=5] -->
     <string name="call_log_item_count">(%1$d)</string>
+
+    <!-- Hint text in the group name box in the edit group view. [CHAR LIMIT=20]-->
+    <string name="group_name_hint">Group\'s Name</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 3fa411c..9a73373 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -71,6 +71,10 @@
         <item name="call_log_secondary_text_color">#FFFFFF</item>
         <item name="call_log_secondary_background_color">#333333</item>
         <item name="call_log_header_color">#33b5e5</item>
+        <!-- VoicemailStatus -->
+        <item name="call_log_voicemail_status_height">40dip</item>
+        <item name="call_log_voicemail_status_background_color">#FFFFE0</item>
+        <item name="call_log_voicemail_status_text_color">#000000</item>
     </style>
 
     <style name="ContactDetailActivityTheme" parent="android:Theme.Holo.Light">
diff --git a/src/com/android/contacts/ContactTileLoaderFactory.java b/src/com/android/contacts/ContactTileLoaderFactory.java
index dc4c935..20ad26e 100644
--- a/src/com/android/contacts/ContactTileLoaderFactory.java
+++ b/src/com/android/contacts/ContactTileLoaderFactory.java
@@ -19,6 +19,8 @@
 
 import android.content.Context;
 import android.content.CursorLoader;
+import android.net.Uri;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 
 /**
@@ -44,6 +46,13 @@
         return new CursorLoader(context, Contacts.CONTENT_STREQUENT_URI, COLUMNS, null, null, null);
     }
 
+    public static CursorLoader createStrequentPhoneOnlyLoader(Context context) {
+        Uri uri = Contacts.CONTENT_STREQUENT_URI.buildUpon()
+                .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true").build();
+
+        return new CursorLoader(context, uri, COLUMNS, null, null, null);
+    }
+
     public static CursorLoader createStarredLoader(Context context) {
         return new CursorLoader(context, Contacts.CONTENT_URI, COLUMNS,
                 Contacts.STARRED + "=?", new String[]{"1"}, Contacts.DISPLAY_NAME + " ASC");
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index de262b2..14378d9 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -141,9 +141,14 @@
             }
         }
 
-        ActionBar actionBar =  getActionBar();
+        // We want the UP affordance but no app icon.
+        // Setting HOME_AS_UP, SHOW_TITLE and clearing SHOW_HOME does the trick.
+        ActionBar actionBar = getActionBar();
         if (actionBar != null) {
-            actionBar.setDisplayHomeAsUpEnabled(true);
+            actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE,
+                    ActionBar.DISPLAY_HOME_AS_UP | ActionBar.DISPLAY_SHOW_TITLE
+                    | ActionBar.DISPLAY_SHOW_HOME);
+            actionBar.setTitle("");
         }
 
         Log.i(TAG, getIntent().getData().toString());
@@ -293,6 +298,7 @@
         @Override
         public void onEditRequested(Uri contactLookupUri) {
             startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri));
+            finish();
         }
 
         @Override
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index 6655c81..28bc3a3 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -148,9 +148,11 @@
         }
 
         @Override
-        public void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome) {
-            setResult(resultCode, resultIntent);
-            if (navigateHome) {
+        public void onSaveFinished(Intent resultIntent) {
+            if (resultIntent != null) {
+                startActivity(resultIntent);
+            } else {
+                // Navigate home
                 Intent intent = new Intent(ContactEditorActivity.this, PeopleActivity.class);
                 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                 startActivity(intent);
@@ -165,7 +167,6 @@
 
         @Override
         public void onContactNotFound() {
-            setResult(Activity.RESULT_CANCELED, null);
             finish();
         }
 
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 2209ab9..62b8321 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -334,7 +334,6 @@
         } else if (fragment instanceof ContactTileListFragment) {
             mStrequentFragment = (ContactTileListFragment) fragment;
             mStrequentFragment.enableQuickContact(false);
-            mStrequentFragment.enableSecondaryTarget(true);
             mStrequentFragment.setListener(mStrequentListener);
         } else if (fragment instanceof PhoneNumberPickerFragment) {
             mSearchFragment = (PhoneNumberPickerFragment) fragment;
diff --git a/src/com/android/contacts/activities/GroupDetailActivity.java b/src/com/android/contacts/activities/GroupDetailActivity.java
index dcd35fa..9f6aa90 100644
--- a/src/com/android/contacts/activities/GroupDetailActivity.java
+++ b/src/com/android/contacts/activities/GroupDetailActivity.java
@@ -18,18 +18,30 @@
 
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
+import com.android.contacts.group.GroupDetailDisplayUtils;
 import com.android.contacts.group.GroupDetailFragment;
 
 import android.app.ActionBar;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.text.TextUtils;
+import android.view.Menu;
+import android.view.MenuInflater;
 import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
 
 public class GroupDetailActivity extends ContactsActivity {
 
     private static final String TAG = "GroupDetailActivity";
 
+    private boolean mShowGroupSourceInActionBar;
+
+    private String mAccountTypeString;
+    private String mGroupSourceAction;
+    private String mGroupSourceUri;
+
     @Override
     public void onCreate(Bundle savedState) {
         super.onCreate(savedState);
@@ -39,9 +51,13 @@
 
         setContentView(R.layout.group_detail_activity);
 
+        mShowGroupSourceInActionBar = getResources().getBoolean(
+                R.bool.config_show_group_action_in_action_bar);
+
         GroupDetailFragment fragment = (GroupDetailFragment) getFragmentManager().findFragmentById(
                 R.id.group_detail_fragment);
         fragment.setListener(mFragmentListener);
+        fragment.setShowGroupSourceInActionBar(mShowGroupSourceInActionBar);
         fragment.loadGroup(getIntent().getData());
         fragment.closeActivityAfterDelete(true);
 
@@ -65,6 +81,15 @@
         }
 
         @Override
+        public void onGroupSourceUpdated(
+                String accountTypeString, String groupSourceAction, String groupSourceActionUri) {
+            mAccountTypeString = accountTypeString;
+            mGroupSourceAction = groupSourceAction;
+            mGroupSourceUri = groupSourceActionUri;
+            invalidateOptionsMenu();
+        }
+
+        @Override
         public void onEditRequested(Uri groupUri) {
             final Intent intent = new Intent(GroupDetailActivity.this, GroupEditorActivity.class);
             intent.setData(groupUri);
@@ -76,11 +101,49 @@
         public void onContactSelected(Uri contactUri) {
             startActivity(new Intent(Intent.ACTION_VIEW, contactUri));
         }
+
     };
 
     @Override
-    public boolean onOptionsItemSelected(MenuItem item) {
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        if (mShowGroupSourceInActionBar) {
+            MenuInflater inflater = getMenuInflater();
+            inflater.inflate(R.menu.group_source, menu);
+        }
+        return true;
+    }
 
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        if (!mShowGroupSourceInActionBar) {
+            return false;
+        }
+        MenuItem groupSourceMenuItem = menu.findItem(R.id.menu_group_source);
+        if (groupSourceMenuItem == null) {
+            return false;
+        }
+        if (TextUtils.isEmpty(mAccountTypeString) || TextUtils.isEmpty(mGroupSourceAction) ||
+                TextUtils.isEmpty(mGroupSourceUri)) {
+            groupSourceMenuItem.setVisible(false);
+            return false;
+        }
+        View groupSourceView = GroupDetailDisplayUtils.getNewGroupSourceView(this);
+        GroupDetailDisplayUtils.bindGroupSourceView(this, groupSourceView,
+                mAccountTypeString);
+        groupSourceView.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                startActivity(new Intent(mGroupSourceAction, Uri.parse(mGroupSourceUri)));
+            }
+        });
+        groupSourceMenuItem.setActionView(groupSourceView);
+        groupSourceMenuItem.setVisible(true);
+        return true;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
         switch (item.getItemId()) {
             case android.R.id.home:
                 finish();
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 5d60f82..0b403cb 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -106,12 +106,10 @@
     private static final String TAG = "PeopleActivity";
     private static final Boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE
 
-    private static final int SUBACTIVITY_NEW_CONTACT = 2;
-    private static final int SUBACTIVITY_EDIT_CONTACT = 3;
-    private static final int SUBACTIVITY_NEW_GROUP = 4;
-    private static final int SUBACTIVITY_EDIT_GROUP = 5;
-    private static final int SUBACTIVITY_ACCOUNT_FILTER = 6;
-    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 7;
+    private static final int SUBACTIVITY_NEW_GROUP = 2;
+    private static final int SUBACTIVITY_EDIT_GROUP = 3;
+    private static final int SUBACTIVITY_ACCOUNT_FILTER = 4;
+    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 5;
 
     private static final String KEY_SEARCH_MODE = "searchMode";
 
@@ -361,11 +359,6 @@
         if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
             // Prepare 2-pane only fragments/views...
 
-            // If setting 1-pane properties, make sure the fragment is created first
-            // by moving code after call to executePendingTransactions
-            mFavoritesFragment.setDisplayType(DisplayType.STARRED_ONLY);
-            mFavoritesFragment.enableQuickContact(true);
-
             // Container views for fragments
             mFavoritesView = getView(R.id.favorites_view);
             mDetailsView = getView(R.id.details_view);
@@ -398,6 +391,14 @@
         transaction.commit();
         fragmentManager.executePendingTransactions();
 
+        // Setting Properties after fragment is created
+        if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
+            mFavoritesFragment.enableQuickContact(true);
+            mFavoritesFragment.setDisplayType(DisplayType.STARRED_ONLY);
+        } else {
+            mFavoritesFragment.setDisplayType(DisplayType.STREQUENT);
+        }
+
         // Configure action bar
         mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar());
         mActionBarAdapter.initialize(savedState, mRequest);
@@ -539,7 +540,8 @@
     public void onAction(Action action) {
         switch (action) {
             case START_SEARCH_MODE:
-                clearSearch();
+                // Tell the fragments that we're in the search mode
+                configureFragments(false /* from request */);
                 updateFragmentsVisibility();
                 invalidateOptionsMenu();
                 break;
@@ -922,7 +924,7 @@
             if (extras != null) {
                 intent.putExtras(extras);
             }
-            startActivityForResult(intent, SUBACTIVITY_EDIT_CONTACT);
+            startActivity(intent);
         }
 
         @Override
@@ -1008,8 +1010,7 @@
 
         @Override
         public void onEditRequested(Uri contactLookupUri) {
-            startActivityForResult(
-                    new Intent(Intent.ACTION_EDIT, contactLookupUri), SUBACTIVITY_EDIT_CONTACT);
+            startActivity(new Intent(Intent.ACTION_EDIT, contactLookupUri));
         }
 
         @Override
@@ -1107,6 +1108,13 @@
         }
 
         @Override
+        public void onGroupSourceUpdated(
+                String accountTypeString, String groupSourceAction, String groupSourceUri) {
+            // Nothing needs to be done here because the group source will be displayed in the
+            // detail fragment
+        }
+
+        @Override
         public void onEditRequested(Uri groupUri) {
             final Intent intent = new Intent(PeopleActivity.this, GroupEditorActivity.class);
             intent.setData(groupUri);
@@ -1286,7 +1294,7 @@
             }
             case R.id.menu_add_contact: {
                 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
-                startActivityForResult(intent, SUBACTIVITY_NEW_CONTACT);
+                startActivity(intent);
                 return true;
             }
             case R.id.menu_add_group: {
@@ -1376,14 +1384,6 @@
                 }
                 break;
             }
-            case SUBACTIVITY_EDIT_CONTACT:
-            case SUBACTIVITY_NEW_CONTACT: {
-                if (resultCode == RESULT_OK && PhoneCapabilityTester.isUsingTwoPanes(this)) {
-                    mRequest.setActionCode(ContactsRequest.ACTION_VIEW_CONTACT);
-                    mAllFragment.reloadDataAndSetSelectedUri(data.getData());
-                }
-                break;
-            }
 
             case SUBACTIVITY_NEW_GROUP:
             case SUBACTIVITY_EDIT_GROUP: {
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index c04e62b..32cc412 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -113,6 +113,13 @@
         /** The value of the "section" column for the items of the old section. */
         public static final int SECTION_OLD_ITEM = 3;
 
+        /** The call log projection including the section name. */
+        public static final String[] EXTENDED_PROJECTION;
+        static {
+            EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
+            System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
+            EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
+        }
 
         public static boolean isSectionHeader(Cursor cursor) {
             int section = cursor.getInt(CallLogQuery.SECTION);
@@ -176,9 +183,14 @@
         public static ContactInfo EMPTY = new ContactInfo();
     }
 
+    public interface GroupCreator {
+        public void addGroup(int cursorPosition, int size, boolean expanded);
+    }
+
     /** Adapter class to fill in data for the Call Log */
     public final class CallLogAdapter extends GroupingListAdapter
-            implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener {
+            implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener,
+            GroupCreator {
         /** The time in millis to delay starting the thread processing requests. */
         private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
 
@@ -507,12 +519,6 @@
             mCallLogGroupBuilder.addGroups(cursor);
         }
 
-        /** Expands visibility to this package. */
-        @Override
-        protected void addGroup(int cursorPosition, int size, boolean expanded) {
-            super.addGroup(cursorPosition, size, expanded);
-        }
-
         @VisibleForTesting
         @Override
         public View newStandAloneView(Context context, ViewGroup parent) {
@@ -730,6 +736,11 @@
         public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
             mContactInfoCache.put(number, contactInfo);
         }
+
+        @Override
+        public void addGroup(int cursorPosition, int size, boolean expanded) {
+            super.addGroup(cursorPosition, size, expanded);
+        }
     }
 
     @Override
diff --git a/src/com/android/contacts/calllog/CallLogGroupBuilder.java b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
index b12fe19..95d85da 100644
--- a/src/com/android/contacts/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
@@ -32,9 +32,9 @@
     /** Reusable char array buffer. */
     private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
 
-    private final CallLogFragment.CallLogAdapter mAdapter;
+    private final CallLogFragment.GroupCreator mAdapter;
 
-    public CallLogGroupBuilder(CallLogFragment.CallLogAdapter adapter) {
+    public CallLogGroupBuilder(CallLogFragment.GroupCreator adapter) {
         mAdapter = adapter;
     }
 
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index a56f778..62e308c 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -95,18 +95,10 @@
         mFragment = new WeakReference<CallLogFragment>(fragment);
     }
 
-    /** Returns the list of columns for the headers. */
-    private String[] getHeaderColumns() {
-        int length = CallLogQuery._PROJECTION.length;
-        String[] columns = new String[length + 1];
-        System.arraycopy(CallLogQuery._PROJECTION, 0, columns, 0, length);
-        columns[length] = CallLogQuery.SECTION_NAME;
-        return columns;
-    }
-
     /** Creates a cursor that contains a single row and maps the section to the given value. */
     private Cursor createHeaderCursorFor(int section) {
-        MatrixCursor matrixCursor = new MatrixCursor(getHeaderColumns());
+        MatrixCursor matrixCursor =
+                new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
         // The values in this row correspond to default values for _PROJECTION from CallLogQuery
         // plus the section value.
         matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", section });
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index 9b89f84..6afe8b9 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -970,7 +970,6 @@
             case SaveMode.CLOSE:
             case SaveMode.HOME:
                 final Intent resultIntent;
-                final int resultCode;
                 if (success && contactLookupUri != null) {
                     final String requestAuthority =
                             mLookupUri == null ? null : mLookupUri.getAuthority();
@@ -978,6 +977,7 @@
                     final String legacyAuthority = "contacts";
 
                     resultIntent = new Intent();
+                    resultIntent.setAction(Intent.ACTION_VIEW);
                     if (legacyAuthority.equals(requestAuthority)) {
                         // Build legacy Uri when requested by caller
                         final long contactId = ContentUris.parseId(Contacts.lookupContact(
@@ -991,15 +991,12 @@
                         resultIntent.setData(contactLookupUri);
                     }
 
-                    resultCode = Activity.RESULT_OK;
                 } else {
-                    resultCode = Activity.RESULT_CANCELED;
                     resultIntent = null;
                 }
                 // It is already saved, so prevent that it is saved again
                 mStatus = Status.CLOSING;
-                if (mListener != null) mListener.onSaveFinished(resultCode, resultIntent,
-                        saveMode == SaveMode.HOME);
+                if (mListener != null) mListener.onSaveFinished(resultIntent);
                 break;
 
             case SaveMode.RELOAD:
@@ -1101,7 +1098,7 @@
         /**
          * Contact was saved and the Fragment can now be closed safely.
          */
-        void onSaveFinished(int resultCode, Intent resultIntent, boolean navigateHome);
+        void onSaveFinished(Intent resultIntent);
 
         /**
          * User decided to delete the contact.
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index 29aa737..cc15213 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -18,8 +18,10 @@
 
 import com.android.contacts.GroupMetaData;
 import com.android.contacts.R;
-import com.android.contacts.list.ContactListPinnedHeaderView;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
 
+import android.accounts.Account;
 import android.content.ContentUris;
 import android.content.Context;
 import android.net.Uri;
@@ -41,8 +43,9 @@
  */
 public class GroupBrowseListAdapter extends BaseAdapter {
 
-    private Context mContext;
-    private LayoutInflater mLayoutInflater;
+    private final Context mContext;
+    private final LayoutInflater mLayoutInflater;
+    private final AccountTypeManager mAccountTypeManager;
 
     private List<GroupListEntry> mGroupList = new ArrayList<GroupListEntry>();
     private boolean mSelectionVisible;
@@ -54,14 +57,16 @@
 
     private static final int VIEW_TYPE_COUNT = ViewType.values().length;
 
-    public GroupBrowseListAdapter(Context context, Map<String, List<GroupMetaData>> groupMap) {
+    public GroupBrowseListAdapter(Context context, Map<Account, List<GroupMetaData>> groupMap) {
         mContext = context;
         mLayoutInflater = LayoutInflater.from(context);
-        for (String accountName : groupMap.keySet()) {
-            List<GroupMetaData> groupsListForAccount = groupMap.get(accountName);
+        mAccountTypeManager = AccountTypeManager.getInstance(mContext);
 
-            // Add account name as header for section
-            mGroupList.add(GroupListEntry.createEntryForHeader(accountName,
+        for (Account account : groupMap.keySet()) {
+            List<GroupMetaData> groupsListForAccount = groupMap.get(account);
+
+            // Add account name, type, and # of groups as header for section
+            mGroupList.add(GroupListEntry.createEntryForHeader(account.name, account.type,
                     groupsListForAccount.size()));
 
             // Add groups within that account as subsequent list items.
@@ -151,15 +156,23 @@
     }
 
     private View getHeaderView(GroupListEntry entry, View convertView, ViewGroup parent) {
-        ContactListPinnedHeaderView result = (ContactListPinnedHeaderView) (convertView == null ?
-                new ContactListPinnedHeaderView(mContext, null) :
+        View result = (convertView == null ?
+                mLayoutInflater.inflate(R.layout.group_list_header_item, parent, false) :
                 convertView);
+
+        TextView accountTypeTextView = (TextView) result.findViewById(R.id.account_type);
+        AccountType accountType = mAccountTypeManager.getAccountType(entry.accountType);
+        accountTypeTextView.setText(accountType.getDisplayLabel(mContext).toString().toUpperCase());
+
+        TextView accountNameTextView = (TextView) result.findViewById(R.id.account_name);
+        accountNameTextView.setText(entry.accountName);
+
         String groupCountString = mContext.getResources().getQuantityString(
                 R.plurals.num_groups_in_account, entry.count, entry.count);
-        // TODO: Format this correctly by using 2 TextViews when the
-        // ContactListPinnedHeaderView is refactored.
-        result.setSectionHeader(entry.title + " " + groupCountString);
-                        return result;
+        TextView groupCountTextView = (TextView) result.findViewById(R.id.group_count);
+        groupCountTextView.setText(groupCountString);
+
+        return result;
     }
 
     private View getGroupListItemView(GroupListEntry entry, View convertView, ViewGroup parent) {
@@ -179,7 +192,8 @@
      */
     public static class GroupListEntry {
         public final ViewType type;
-        public final String title;
+        public final String accountType;
+        public final String accountName;
         public final int count;
         public final GroupMetaData groupData;
         /**
@@ -188,24 +202,27 @@
          */
         public final long id;
 
-        private GroupListEntry(ViewType entryType, String headerTitle, int headerGroupCount,
-                GroupMetaData groupMetaData, long entryId) {
+        private GroupListEntry(ViewType entryType, String groupAccountName, String groupAccountType,
+                int headerGroupCount, GroupMetaData groupMetaData, long entryId) {
             type = entryType;
-            title = headerTitle;
+            accountName = groupAccountName;
+            accountType = groupAccountType;
             count = headerGroupCount;
             groupData = groupMetaData;
             id = entryId;
         }
 
-        public static GroupListEntry createEntryForHeader(String headerTitle, int groupCount) {
-            return new GroupListEntry(ViewType.HEADER, headerTitle, groupCount, null, -1);
+        public static GroupListEntry createEntryForHeader(String groupAccountName,
+                String groupAccountType, int groupCount) {
+            return new GroupListEntry(ViewType.HEADER, groupAccountName, groupAccountType,
+                    groupCount, null, -1);
         }
 
         public static GroupListEntry createEntryForGroup(GroupMetaData groupMetaData) {
             if (groupMetaData == null) {
                 throw new IllegalStateException("Cannot create list entry for a null group");
             }
-            return new GroupListEntry(ViewType.ITEM, null, 0, groupMetaData,
+            return new GroupListEntry(ViewType.ITEM, null, null, 0, groupMetaData,
                     groupMetaData.getGroupId());
         }
     }
diff --git a/src/com/android/contacts/group/GroupBrowseListFragment.java b/src/com/android/contacts/group/GroupBrowseListFragment.java
index 0b53acf..4e6bdbc 100644
--- a/src/com/android/contacts/group/GroupBrowseListFragment.java
+++ b/src/com/android/contacts/group/GroupBrowseListFragment.java
@@ -22,6 +22,7 @@
 import com.android.contacts.group.GroupBrowseListAdapter.GroupListItem;
 import com.android.contacts.widget.AutoScrollListView;
 
+import android.accounts.Account;
 import android.app.Activity;
 import android.app.Fragment;
 import android.app.LoaderManager;
@@ -82,12 +83,11 @@
     private static final String EXTRA_KEY_GROUP_URI = "groups.groupUri";
 
     /**
-     * Map of account name to a list of {@link GroupMetaData} objects
+     * Map of {@link Account} to a list of {@link GroupMetaData} objects
      * representing groups within that account.
-     * TODO: Change account name string into a wrapper object that has
-     * account name, type, and authority.
      */
-    private Map<String, List<GroupMetaData>> mGroupMap = new HashMap<String, List<GroupMetaData>>();
+    private final Map<Account, List<GroupMetaData>> mGroupMap =
+            new HashMap<Account, List<GroupMetaData>>();
 
     private View mRootView;
     private AutoScrollListView mListView;
@@ -214,14 +214,15 @@
 
             GroupMetaData newGroup = new GroupMetaData(accountName, accountType, groupId, title,
                     defaultGroup, favorites);
+            Account account = new Account(accountName, accountType);
 
-            if (mGroupMap.containsKey(accountName)) {
-                List<GroupMetaData> groups = mGroupMap.get(accountName);
+            if (mGroupMap.containsKey(account)) {
+                List<GroupMetaData> groups = mGroupMap.get(account);
                 groups.add(newGroup);
             } else {
                 List<GroupMetaData> groups = new ArrayList<GroupMetaData>();
                 groups.add(newGroup);
-                mGroupMap.put(accountName, groups);
+                mGroupMap.put(account, groups);
             }
 
         }
diff --git a/src/com/android/contacts/group/GroupDetailDisplayUtils.java b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
new file mode 100644
index 0000000..56413b5
--- /dev/null
+++ b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.group;
+
+import com.android.contacts.R;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+
+public class GroupDetailDisplayUtils {
+
+    private GroupDetailDisplayUtils() {
+        // Disallow explicit creation of this class.
+    }
+
+    public static View getNewGroupSourceView(Context context) {
+        LayoutInflater inflater = (LayoutInflater)context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        return inflater.inflate(R.layout.group_source_button, null);
+    }
+
+    public static void bindGroupSourceView(Context context, View view, String accountTypeString) {
+        ImageView accountIcon = (ImageView) view.findViewById(android.R.id.icon);
+        if (accountIcon == null) {
+            throw new IllegalStateException("Group source view must contain view with id"
+                    + "android.R.id.icon");
+        }
+        AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(context);
+        AccountType accountType = accountTypeManager.getAccountType(accountTypeString);
+        accountIcon.setImageDrawable(accountType.getDisplayIcon(context));
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/group/GroupDetailFragment.java b/src/com/android/contacts/group/GroupDetailFragment.java
index a2ab23f..576f6c1 100644
--- a/src/com/android/contacts/group/GroupDetailFragment.java
+++ b/src/com/android/contacts/group/GroupDetailFragment.java
@@ -32,17 +32,19 @@
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.Context;
 import android.content.CursorLoader;
+import android.content.Intent;
 import android.content.Loader;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.util.Log;
+import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.widget.AbsListView;
 import android.widget.AbsListView.OnScrollListener;
@@ -66,6 +68,12 @@
         public void onGroupSizeUpdated(String size);
 
         /**
+         * The group source (intent action and action URI) has been determined.
+         */
+        public void onGroupSourceUpdated(String accountTypeString, String groupSourceAction,
+                String groupSourceUri);
+
+        /**
          * User decided to go to Edit-Mode
          */
         public void onEditRequested(Uri groupUri);
@@ -84,6 +92,8 @@
     private Context mContext;
 
     private View mRootView;
+    private ViewGroup mGroupSourceViewContainer;
+    private View mGroupSourceView;
     private TextView mGroupTitle;
     private TextView mGroupSize;
     private ListView mMemberListView;
@@ -98,8 +108,11 @@
     private long mGroupId;
     private String mGroupName;
     private String mAccountTypeString;
+    private boolean mIsReadOnly;
 
+    private boolean mShowGroupActionInActionBar;
     private boolean mOptionsMenuEditable;
+    private boolean mOptionsMenuGroupPresent;
     private boolean mCloseActivityAfterDelete;
 
     public GroupDetailFragment() {
@@ -132,6 +145,8 @@
         mRootView = inflater.inflate(R.layout.group_detail_fragment, container, false);
         mGroupTitle = (TextView) mRootView.findViewById(R.id.group_title);
         mGroupSize = (TextView) mRootView.findViewById(R.id.group_size);
+        mGroupSourceViewContainer = (ViewGroup) mRootView.findViewById(
+                R.id.group_source_view_container);
         mMemberListView = (ListView) mRootView.findViewById(android.R.id.list);
 
         return mRootView;
@@ -169,6 +184,10 @@
         mListener = value;
     }
 
+    public void setShowGroupSourceInActionBar(boolean show) {
+        mShowGroupActionInActionBar = show;
+    }
+
     /**
      * Start the loader to retrieve the metadata for this group.
      */
@@ -254,12 +273,15 @@
             mAccountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
             mGroupId = cursor.getLong(GroupMetaDataLoader.GROUP_ID);
             mGroupName = cursor.getString(GroupMetaDataLoader.TITLE);
+            mIsReadOnly = cursor.getInt(GroupMetaDataLoader.IS_READ_ONLY) == 1;
             updateTitle(mGroupName);
+            // Must call invalidate so that the option menu will get updated
+            getActivity().invalidateOptionsMenu ();
 
-            // TODO: Replace by real button
-            final String action = cursor.getString(GroupMetaDataLoader.ACTION);
-            final String actionUri = cursor.getString(GroupMetaDataLoader.ACTION_URI);
-            Log.d(TAG, "Group open action: " + action + ", uri: " + actionUri);
+            final String accountTypeString = cursor.getString(GroupMetaDataLoader.ACCOUNT_TYPE);
+            final String groupSourceAction = cursor.getString(GroupMetaDataLoader.ACTION);
+            final String groupSourceUri = cursor.getString(GroupMetaDataLoader.ACTION_URI);
+            updateGroupSouce(accountTypeString, groupSourceAction, groupSourceUri);
         }
     }
 
@@ -294,6 +316,56 @@
         }
     }
 
+    /**
+     * Once the account type, group source action, and group source URI have been determined
+     * (based on the result from the {@link Loader}), then we can display this to the user in 1 of
+     * 3 ways depending on screen size and orientation: either as a button in the action bar,
+     * a button in a static header on the page, or as a header that scrolls with the
+     * {@link ListView}.
+     */
+    private void updateGroupSouce(final String accountTypeString, final String groupSourceAction,
+            final String groupSourceUri) {
+
+        // If the group action should be shown in the action bar, then pass the data to the
+        // listener who will take care of setting up the view and click listener. There is nothing
+        // else to be done by this {@link Fragment}.
+        if (mShowGroupActionInActionBar) {
+            mListener.onGroupSourceUpdated(accountTypeString, groupSourceAction, groupSourceUri);
+            return;
+        }
+
+        // Otherwise, if the {@link Fragment} needs to create and setup the button, then first
+        // verify that there is a valid action.
+        if (!TextUtils.isEmpty(groupSourceAction) && !TextUtils.isEmpty(groupSourceUri)) {
+            if (mGroupSourceView == null) {
+                mGroupSourceView = GroupDetailDisplayUtils.getNewGroupSourceView(mContext);
+                // Figure out how to add the view to the fragment.
+                // If there is a static header with a container for the group source view, insert
+                // the view there.
+                if (mGroupSourceViewContainer != null) {
+                    mGroupSourceViewContainer.addView(mGroupSourceView);
+                } else {
+                    // Otherwise, display the group source as a scrolling header within the
+                    // {@link ListView} of group members.
+                    mMemberListView.addHeaderView(mGroupSourceView);
+                }
+            }
+
+            // Rebind the data since this action can change if the loader returns updated data
+            mGroupSourceView.setVisibility(View.VISIBLE);
+            GroupDetailDisplayUtils.bindGroupSourceView(mContext, mGroupSourceView,
+                    accountTypeString);
+            mGroupSourceView.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    startActivity(new Intent(groupSourceAction, Uri.parse(groupSourceUri)));
+                }
+            });
+        } else if (mGroupSourceView != null) {
+            mGroupSourceView.setVisibility(View.GONE);
+        }
+    }
+
     @Override
     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
             int totalItemCount) {
@@ -314,21 +386,27 @@
     }
 
     public boolean isOptionsMenuChanged() {
-        return mOptionsMenuEditable != isGroupEditable();
+        return mOptionsMenuEditable != isGroupEditable() &&
+                mOptionsMenuGroupPresent != isGroupPresent();
     }
 
     public boolean isGroupEditable() {
-        // TODO: This should check the group_is_read_only flag. Modify GroupMetaDataLoader.
-        // Bug: 4601729.
+        return mGroupUri != null && !mIsReadOnly;
+    }
+
+    public boolean isGroupPresent() {
         return mGroupUri != null;
     }
 
     @Override
     public void onPrepareOptionsMenu(Menu menu) {
         mOptionsMenuEditable = isGroupEditable();
+        mOptionsMenuGroupPresent = isGroupPresent();
 
+        // Editing a group is always possible if a group is selected
+        // TODO: check for external group (member editable) buganizer #5049046
         final MenuItem editMenu = menu.findItem(R.id.menu_edit_group);
-        editMenu.setVisible(mOptionsMenuEditable);
+        editMenu.setVisible(mOptionsMenuGroupPresent);
 
         final MenuItem deleteMenu = menu.findItem(R.id.menu_delete_group);
         deleteMenu.setVisible(mOptionsMenuEditable);
diff --git a/src/com/android/contacts/list/ContactTileAdapter.java b/src/com/android/contacts/list/ContactTileAdapter.java
index 65b99cc..596cceb 100644
--- a/src/com/android/contacts/list/ContactTileAdapter.java
+++ b/src/com/android/contacts/list/ContactTileAdapter.java
@@ -61,7 +61,6 @@
     private int mStarredIndex;
 
     private boolean mIsQuickContactEnabled = false;
-    private boolean mIsSecondaryTargetEnabled = false;
 
     /**
      * Configures the adapter to filter and display contacts using different view types.
@@ -69,11 +68,17 @@
      */
     public enum DisplayType {
         /**
-         * Displays a mixed view type of starred without secondary target and frequent contacts
+         * Displays a mixed view type of starred and frequent contacts
          */
         STREQUENT,
 
         /**
+         * Displays a mixed view type of starred and frequent contacts based on phone data.
+         * Also includes secondary touch target.
+         */
+        STREQUENT_PHONE_ONLY,
+
+        /**
          * Display only starred contacts
          */
         STARRED_ONLY,
@@ -117,10 +122,6 @@
         mIsQuickContactEnabled = enableQuickContact;
     }
 
-    public void enableSecondaryTarget(boolean enableSecondaryTarget) {
-        mIsSecondaryTargetEnabled = enableSecondaryTarget;
-    }
-
     /**
      * Sets the column indices for expected {@link Cursor}
      * based on {@link DisplayType}.
@@ -161,18 +162,22 @@
     /**
      * Iterates over the {@link Cursor}
      * Returns position of the first NON Starred Contact
-     * Returns -1 if not {@link DisplayType#STREQUENT}
+     * Returns -1 if not {@link DisplayType#}
      */
     private int getDividerPosition(Cursor cursor) {
-      if (cursor == null || cursor.isClosed() || mDisplayType != DisplayType.STREQUENT) {
-          return -1;
-      }
-      while (cursor.moveToNext()) {
-          if (cursor.getInt(mStarredIndex) == 0) {
-              return cursor.getPosition();
-          }
-      }
-      return -1;
+        if (cursor == null || cursor.isClosed() || (mDisplayType != DisplayType.STREQUENT
+                && mDisplayType != DisplayType.STREQUENT_PHONE_ONLY)) {
+            return -1;
+        }
+        while (cursor.moveToNext()) {
+            if (cursor.getInt(mStarredIndex) == 0) {
+                return cursor.getPosition();
+            }
+        }
+
+        // There are not NON Starred contacts in cursor
+        // Set divider positon to end and add 1 to make sure it doesn't get drawn
+        return cursor.getCount() + 1;
     }
 
     private ContactEntry createContactEntryFromCursor(Cursor cursor, int position) {
@@ -205,6 +210,7 @@
             case GROUP_MEMBERS:
                 return getRowCount(mContactCursor.getCount());
             case STREQUENT:
+            case STREQUENT_PHONE_ONLY:
                 /*
                  * Takes numbers of rows the Starred Contacts Occupy
                  * Calculates the number of frequent contacts
@@ -248,6 +254,7 @@
                 }
                 break;
             case STREQUENT:
+            case STREQUENT_PHONE_ONLY:
                 if (position < getRowCount(mDividerPosition)) {
                     for (int columnCounter = 0; columnCounter < mColumnCount &&
                             contactIndex != mDividerPosition; columnCounter++) {
@@ -283,7 +290,8 @@
 
     @Override
     public boolean areAllItemsEnabled() {
-        return mDisplayType != DisplayType.STREQUENT;
+        return (mDisplayType != DisplayType.STREQUENT &&
+                mDisplayType != DisplayType.STREQUENT_PHONE_ONLY);
     }
 
     @Override
@@ -337,7 +345,8 @@
     }
     @Override
     public int getViewTypeCount() {
-        return mDisplayType == DisplayType.STREQUENT ? ViewTypes.COUNT : 1;
+        return (mDisplayType == DisplayType.STREQUENT ||
+                mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) ? ViewTypes.COUNT : 1;
     }
 
     /**
@@ -353,13 +362,20 @@
         switch (mDisplayType) {
             case STREQUENT:
                 if (position < getRowCount(mDividerPosition)) {
-                    return (mIsSecondaryTargetEnabled ?
-                            ViewTypes.STARRED_WITH_SECONDARY_ACTION : ViewTypes.STARRED);
+                    return ViewTypes.STARRED;
                 } else if (position == getRowCount(mDividerPosition)) {
                     return ViewTypes.DIVIDER;
                 } else {
                     return ViewTypes.FREQUENT;
                 }
+            case STREQUENT_PHONE_ONLY:
+                if (position < getRowCount(mDividerPosition)) {
+                    return ViewTypes.STARRED_WITH_SECONDARY_ACTION;
+                 } else if (position == getRowCount(mDividerPosition)) {
+                    return ViewTypes.DIVIDER;
+                } else {
+                    return ViewTypes.FREQUENT;
+                }
             case STARRED_ONLY:
             case GROUP_MEMBERS:
                 return ViewTypes.STARRED;
diff --git a/src/com/android/contacts/list/ContactTileListFragment.java b/src/com/android/contacts/list/ContactTileListFragment.java
index a36f358..f4bd1b5 100644
--- a/src/com/android/contacts/list/ContactTileListFragment.java
+++ b/src/com/android/contacts/list/ContactTileListFragment.java
@@ -50,7 +50,7 @@
     private Listener mListener;
     private ContactTileAdapter mAdapter;
     private ListView mListView;
-    private DisplayType mDisplayType = DisplayType.STREQUENT;
+    private DisplayType mDisplayType = DisplayType.STREQUENT_PHONE_ONLY;
 
     @Override
     public void onAttach(Activity activity) {
@@ -94,10 +94,6 @@
         mAdapter.enableQuickContact(enableQuickContact);
     }
 
-    public void enableSecondaryTarget(boolean enableSecondaryTarget) {
-        mAdapter.enableSecondaryTarget(enableSecondaryTarget);
-    }
-
     private final LoaderManager.LoaderCallbacks<Cursor> mContactTileLoaderListener =
             new LoaderCallbacks<Cursor>() {
 
@@ -108,6 +104,8 @@
                   return ContactTileLoaderFactory.createStarredLoader(getActivity());
               case STREQUENT:
                   return ContactTileLoaderFactory.createStrequentLoader(getActivity());
+              case STREQUENT_PHONE_ONLY:
+                  return ContactTileLoaderFactory.createStrequentPhoneOnlyLoader(getActivity());
               case FREQUENT_ONLY:
                   return ContactTileLoaderFactory.createFrequentLoader(getActivity());
               default:
diff --git a/src/com/android/contacts/vcard/ImportProcessor.java b/src/com/android/contacts/vcard/ImportProcessor.java
index 1f5779a..2a5583d 100644
--- a/src/com/android/contacts/vcard/ImportProcessor.java
+++ b/src/com/android/contacts/vcard/ImportProcessor.java
@@ -249,7 +249,7 @@
                 // ImportVCardActivity and ImportVCardService).
                 Log.e(LOG_TAG, "Nested Exception is found.");
             } catch (VCardNotSupportedException e) {
-                Log.e(LOG_TAG, e.getMessage());
+                Log.e(LOG_TAG, e.toString());
             } catch (VCardVersionException e) {
                 if (i == length - 1) {
                     Log.e(LOG_TAG, "Appropriate version for this vCard is not found.");
@@ -257,7 +257,7 @@
                     // We'll try the other (v30) version.
                 }
             } catch (VCardException e) {
-                Log.e(LOG_TAG, e.getMessage());
+                Log.e(LOG_TAG, e.toString());
             } finally {
                 if (is != null) {
                     try {
diff --git a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
new file mode 100644
index 0000000..f8da9c8
--- /dev/null
+++ b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import static com.google.android.collect.Lists.newArrayList;
+
+import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
+
+import android.database.MatrixCursor;
+import android.provider.CallLog.Calls;
+import android.test.AndroidTestCase;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link CallLogGroupBuilder}
+ */
+public class CallLogGroupBuilderTest extends AndroidTestCase {
+    /** A phone number for testing. */
+    private static final String TEST_NUMBER1 = "14125551234";
+    /** A phone number for testing. */
+    private static final String TEST_NUMBER2 = "14125555555";
+
+    /** The object under test. */
+    private CallLogGroupBuilder mBuilder;
+    /** Records the created groups. */
+    private FakeGroupCreator mFakeGroupCreator;
+    /** Cursor to store the values. */
+    private MatrixCursor mCursor;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mFakeGroupCreator = new FakeGroupCreator();
+        mBuilder = new CallLogGroupBuilder(mFakeGroupCreator);
+        mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        mCursor = null;
+        mBuilder = null;
+        mFakeGroupCreator = null;
+        super.tearDown();
+    }
+
+    public void testAddGroups_NoCalls() {
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_OneCall() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_TwoCallsNotMatching() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER2, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(0, mFakeGroupCreator.groups.size());
+    }
+
+    public void testAddGroups_ThreeCallsMatching() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(1, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+    }
+
+    public void testAddGroups_MatchingIncomingAndOutgoing() {
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.OUTGOING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(1, mFakeGroupCreator.groups.size());
+        assertGroupIs(0, 3, false, mFakeGroupCreator.groups.get(0));
+    }
+
+    public void testAddGroups_HeaderSplitsGroups() {
+        addNewCallLogHeader();
+        addNewCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addNewCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogHeader();
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        addOldCallLogEntry(TEST_NUMBER1, Calls.INCOMING_TYPE);
+        mBuilder.addGroups(mCursor);
+        assertEquals(2, mFakeGroupCreator.groups.size());
+        assertGroupIs(1, 2, false, mFakeGroupCreator.groups.get(0));
+        assertGroupIs(4, 2, false, mFakeGroupCreator.groups.get(1));
+    }
+
+    private void addOldCallLogEntry(String number, int type) {
+        addCallLogEntry(number, type, CallLogQuery.SECTION_OLD_ITEM);
+    }
+
+    private void addNewCallLogEntry(String number, int type) {
+        addCallLogEntry(number, type, CallLogQuery.SECTION_NEW_ITEM);
+    }
+
+    /** Adds a call log entry with the given number and type to the cursor. */
+    private void addCallLogEntry(String number, int type, int section) {
+        if (section != CallLogQuery.SECTION_NEW_ITEM
+                && section != CallLogQuery.SECTION_OLD_ITEM) {
+            throw new IllegalArgumentException("not an item section: " + section);
+        }
+        mCursor.moveToNext();
+        mCursor.addRow(new Object[]{
+                mCursor.getPosition(), number, 0L, 0L, type, "", "", section
+        });
+    }
+
+    private void addOldCallLogHeader() {
+        addCallLogHeader(CallLogQuery.SECTION_OLD_HEADER);
+    }
+
+    private void addNewCallLogHeader() {
+        addCallLogHeader(CallLogQuery.SECTION_NEW_HEADER);
+    }
+
+    /** Adds a call log entry with a header to the cursor. */
+    private void addCallLogHeader(int section) {
+        if (section != CallLogQuery.SECTION_NEW_HEADER
+                && section != CallLogQuery.SECTION_OLD_HEADER) {
+            throw new IllegalArgumentException("not a header section: " + section);
+        }
+        mCursor.moveToNext();
+        mCursor.addRow(new Object[]{ mCursor.getPosition(), "", 0L, 0L, 0, "", "", section });
+    }
+
+    /** Asserts that the group matches the given values. */
+    private void assertGroupIs(int cursorPosition, int size, boolean expanded, GroupSpec group) {
+        assertEquals(cursorPosition, group.cursorPosition);
+        assertEquals(size, group.size);
+        assertEquals(expanded, group.expanded);
+    }
+
+    private static class GroupSpec {
+        public final int cursorPosition;
+        public final int size;
+        public final boolean expanded;
+
+        public GroupSpec(int cursorPosition, int size, boolean expanded) {
+            this.cursorPosition = cursorPosition;
+            this.size = size;
+            this.expanded = expanded;
+        }
+    }
+
+    private static class FakeGroupCreator implements CallLogFragment.GroupCreator {
+        public final List<GroupSpec> groups = newArrayList();
+        @Override
+        public void addGroup(int cursorPosition, int size, boolean expanded) {
+            groups.add(new GroupSpec(cursorPosition, size, expanded));
+        }
+    }
+}