Merge "Avoid showing error if contact removed on save."
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index df65877..18782b9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -503,6 +503,13 @@
             android:windowSoftInputMode="adjustResize"
             android:exported="false"/>
 
+        <!-- Accounts changed prompt that can appear when creating a new contact. -->
+        <activity
+            android:name=".activities.ContactEditorAccountsChangedActivity"
+            android:theme="@style/ContactEditorAccountsChangedActivityTheme"
+            android:windowSoftInputMode="adjustResize"
+            android:exported="false"/>
+
         <!-- Create a new or edit an existing contact -->
         <activity
             android:name=".activities.ContactEditorActivity"
diff --git a/res/drawable-hdpi/badge_action_call.png b/res/drawable-hdpi/badge_action_call.png
old mode 100755
new mode 100644
index 105f7d0..0b1c6b4
--- a/res/drawable-hdpi/badge_action_call.png
+++ b/res/drawable-hdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-hdpi/badge_action_sms.png b/res/drawable-hdpi/badge_action_sms.png
old mode 100755
new mode 100644
index a7862f6..0dfdbf5
--- a/res/drawable-hdpi/badge_action_sms.png
+++ b/res/drawable-hdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-hdpi/ic_menu_overflow.png b/res/drawable-hdpi/ic_menu_overflow.png
index b028095..a12aedf 100644
--- a/res/drawable-hdpi/ic_menu_overflow.png
+++ b/res/drawable-hdpi/ic_menu_overflow.png
Binary files differ
diff --git a/res/drawable-mdpi/badge_action_call.png b/res/drawable-mdpi/badge_action_call.png
index 3bc88a3..af2abaa 100644
--- a/res/drawable-mdpi/badge_action_call.png
+++ b/res/drawable-mdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-mdpi/badge_action_sms.png b/res/drawable-mdpi/badge_action_sms.png
index bbcfb59..13dd8bc 100644
--- a/res/drawable-mdpi/badge_action_sms.png
+++ b/res/drawable-mdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_menu_overflow.png b/res/drawable-mdpi/ic_menu_overflow.png
index 74dd41a..4a3bde3 100644
--- a/res/drawable-mdpi/ic_menu_overflow.png
+++ b/res/drawable-mdpi/ic_menu_overflow.png
Binary files differ
diff --git a/res/drawable-xhdpi/badge_action_call.png b/res/drawable-xhdpi/badge_action_call.png
index aa28018..2589e33 100644
--- a/res/drawable-xhdpi/badge_action_call.png
+++ b/res/drawable-xhdpi/badge_action_call.png
Binary files differ
diff --git a/res/drawable-xhdpi/badge_action_sms.png b/res/drawable-xhdpi/badge_action_sms.png
index 5e4cd82..460451f 100644
--- a/res/drawable-xhdpi/badge_action_sms.png
+++ b/res/drawable-xhdpi/badge_action_sms.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_menu_overflow.png b/res/drawable-xhdpi/ic_menu_overflow.png
index c88c4a4..715cff8 100644
--- a/res/drawable-xhdpi/ic_menu_overflow.png
+++ b/res/drawable-xhdpi/ic_menu_overflow.png
Binary files differ
diff --git a/res/layout-sw580dp/group_source_button.xml b/res/layout-sw580dp/group_source_button.xml
index a058990..e0fe4a9 100644
--- a/res/layout-sw580dp/group_source_button.xml
+++ b/res/layout-sw580dp/group_source_button.xml
@@ -29,12 +29,12 @@
     android:padding="10dip" >
 
     <TextView
+        android:id="@android:id/title"
         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"/>
+        android:textAppearance="?android:attr/textAppearanceMedium"/>
 
     <ImageView
         android:id="@android:id/icon"
diff --git a/res/layout-w470dp/group_source_button.xml b/res/layout-w470dp/group_source_button.xml
index 1acd510..af62c2c 100644
--- a/res/layout-w470dp/group_source_button.xml
+++ b/res/layout-w470dp/group_source_button.xml
@@ -34,12 +34,12 @@
         android:orientation="horizontal">
 
         <TextView
+            android:id="@android:id/title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
             android:textAppearance="?android:attr/textAppearanceMedium"
             android:textColor="@color/action_bar_button_text_color"
-            android:text="@string/view_updates_from_group"
             style="@android:style/Widget.Holo.ActionBar.TabText"/>
 
         <ImageView
diff --git a/res/layout/account_selector_list_item.xml b/res/layout/account_selector_list_item.xml
index a700866..4cba3e9 100644
--- a/res/layout/account_selector_list_item.xml
+++ b/res/layout/account_selector_list_item.xml
@@ -44,6 +44,7 @@
             android:layout_height="wrap_content"
             android:layout_marginRight="8dip"
             android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorSecondary"
             android:singleLine="true"
             android:ellipsize="end"/>
     </LinearLayout>
diff --git a/res/layout/contact_editor_accounts_changed_activity_with_picker.xml b/res/layout/contact_editor_accounts_changed_activity_with_picker.xml
new file mode 100644
index 0000000..a5aab20
--- /dev/null
+++ b/res/layout/contact_editor_accounts_changed_activity_with_picker.xml
@@ -0,0 +1,55 @@
+<?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 account prompt (which includes a ListView) that can appear when
+  the user creates a new contact.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="15dip"
+        android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:background="?android:attr/listDivider"/>
+
+    <ListView android:id="@+id/account_list"
+        android:layout_width="match_parent"
+        android:layout_height="0dip"
+        android:layout_weight="1"
+        android:fadingEdge="none"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:background="?android:attr/listDivider"/>
+
+    <Button
+        android:id="@+id/add_account_button"
+        style="?android:attr/buttonBarButtonStyle"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/res/layout/contact_editor_accounts_changed_activity_with_text.xml b/res/layout/contact_editor_accounts_changed_activity_with_text.xml
new file mode 100644
index 0000000..33714ea
--- /dev/null
+++ b/res/layout/contact_editor_accounts_changed_activity_with_text.xml
@@ -0,0 +1,60 @@
+<?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 account prompt (which just includes text and 2 buttons) that can appear when the user
+  creates a new contact.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView android:id="@+id/text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="15dip"
+        android:textAppearance="?android:attr/textAppearanceMedium"/>
+
+    <View
+        android:layout_width="match_parent"
+        android:layout_height="1dip"
+        android:background="?android:attr/listDivider"/>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        style="?android:attr/buttonBarStyle">
+
+        <Button
+            android:id="@+id/left_button"
+            style="?android:attr/buttonBarButtonStyle"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
+
+        <Button
+            android:id="@+id/right_button"
+            style="?android:attr/buttonBarButtonStyle"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1" />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/editor_account_header.xml b/res/layout/editor_account_header.xml
index 6dd55fd..c255209 100644
--- a/res/layout/editor_account_header.xml
+++ b/res/layout/editor_account_header.xml
@@ -26,7 +26,7 @@
     android:paddingBottom="8dip"
     android:gravity="center_vertical"
     android:paddingLeft="@dimen/account_container_left_padding"
-    android:paddingRight="32dip">
+    android:paddingRight="28dip">
 
     <LinearLayout
         android:id="@+id/account"
diff --git a/res/layout/editor_account_header_with_dropdown.xml b/res/layout/editor_account_header_with_dropdown.xml
index 12c2a84..311a783 100644
--- a/res/layout/editor_account_header_with_dropdown.xml
+++ b/res/layout/editor_account_header_with_dropdown.xml
@@ -24,7 +24,7 @@
     android:orientation="horizontal"
     android:gravity="center_vertical"
     android:paddingLeft="@dimen/account_container_left_padding"
-    android:paddingRight="32dip">
+    android:paddingRight="28dip">
 
     <LinearLayout
         android:id="@+id/account"
diff --git a/res/layout/group_detail_fragment.xml b/res/layout/group_detail_fragment.xml
index d95a6db..2b020c9 100644
--- a/res/layout/group_detail_fragment.xml
+++ b/res/layout/group_detail_fragment.xml
@@ -40,7 +40,8 @@
             android:layout_width="match_parent"
             android:layout_height="match_parent"
             android:fadingEdge="none"
-            android:scrollbarStyle="outsideOverlay"/>
+            android:scrollbarStyle="outsideOverlay"
+            android:divider="@null"/>
 
         <!--
           Shadow overlay over the list of group members (since we have a fake stacked
diff --git a/res/layout/group_source_button.xml b/res/layout/group_source_button.xml
index 49aa2db..8d09033 100644
--- a/res/layout/group_source_button.xml
+++ b/res/layout/group_source_button.xml
@@ -30,6 +30,7 @@
     android:paddingRight="16dip" >
 
     <TextView
+        android:id="@android:id/title"
         android:layout_width="0dip"
         android:layout_height="wrap_content"
         android:layout_weight="1"
@@ -37,7 +38,6 @@
         android:duplicateParentState="true"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:textColor="@color/action_bar_button_text_color"
-        android:text="@string/view_updates_from_group"
         style="@android:style/Widget.Holo.ActionBar.TabText"/>
 
     <FrameLayout
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 561eefc..2fa7005 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -74,4 +74,6 @@
     <!--  Color of the vertical stripe that goes on the left of a block quote inside a stream item -->
     <color name="stream_item_stripe_color">#CCCCCC</color>
 
+    <!-- Color of image view placeholder. -->
+    <color name="image_placeholder">#DDDDDD</color>
 </resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 5a94324..4decc19 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -156,8 +156,12 @@
     <dimen name="contact_browser_list_item_text_indent">8dip</dimen>
     <dimen name="contact_browser_list_top_margin">8dip</dimen>
 
-    <!--  ContactTile Layouts -->
-    <dimen name="contact_tile_shadowbox_height">48dip</dimen>
+    <!-- ContactTile Layouts -->
+    <!--
+      Use sp instead of dip so that the shadowbox heights can all scale uniformly
+      when the font size is scaled for accessibility purposes
+    -->
+    <dimen name="contact_tile_shadowbox_height">48sp</dimen>
 
     <!-- Call Log -->
     <dimen name="call_log_call_action_size">32dip</dimen>
diff --git a/res/values/donottranslate_config.xml b/res/values/donottranslate_config.xml
index 0da6485..79a2b29 100644
--- a/res/values/donottranslate_config.xml
+++ b/res/values/donottranslate_config.xml
@@ -120,4 +120,8 @@
     <!-- Height of the tab carousel as a percentage of the current screen width on the
          contact detail page -->
     <item name="tab_height_screen_width_percentage" type="fraction">50%</item>
+
+    <!-- Regular expression for prohibiting certain phone numbers in dialpad.
+         Ignored if empty. -->
+    <string name="config_prohibited_phone_number_regexp"></string>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ed9dd0f..aa99d21 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1817,4 +1817,8 @@
 
     <!-- Button label to prompt the user to add another account (when there are already existing accounts on the device) [CHAR LIMIT=30] -->
     <string name="add_new_account">Add new account</string>
+
+    <!-- Dialog message which is shown when the user tries to make a phone call
+         to prohibited phone numbers [CHAR LIMIT=NONE] -->
+    <string name="phone_call_prohibited" msgid="4313552620858880999">Call not sent.</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 6a64dfd..e3de72b 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -208,6 +208,12 @@
         <item name="android:windowCloseOnTouchOutside">true</item>
     </style>
 
+    <style name="ContactEditorAccountsChangedActivityTheme" parent="@android:style/Theme.Holo.Light.Dialog.NoActionBar.MinWidth">
+        <item name="android:windowCloseOnTouchOutside">true</item>
+        <item name="android:textColorPrimary">@color/primary_text_color</item>
+        <item name="android:textColorSecondary">@color/secondary_text_color</item>
+    </style>
+
     <style name="SectionDivider">
         <item name="android:background">#7e7e87</item>
         <item name="android:layout_height">1dip</item>
diff --git a/src/com/android/contacts/ContactPhotoManager.java b/src/com/android/contacts/ContactPhotoManager.java
index b2ceffa..e04a07a 100644
--- a/src/com/android/contacts/ContactPhotoManager.java
+++ b/src/com/android/contacts/ContactPhotoManager.java
@@ -28,6 +28,8 @@
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.Handler.Callback;
@@ -67,6 +69,35 @@
         return R.drawable.ic_contact_picture_holo_light;
     }
 
+    public static abstract class DefaultImageProvider {
+        public abstract void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme);
+    }
+
+    private static class AvatarDefaultImageProvider extends DefaultImageProvider {
+        @Override
+        public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
+            view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+        }
+    }
+
+    private static class BlankDefaultImageProvider extends DefaultImageProvider {
+        private static Drawable sDrawable;
+
+        @Override
+        public void applyDefaultImage(ImageView view, boolean hires, boolean darkTheme) {
+            if (sDrawable == null) {
+                Context context = view.getContext();
+                sDrawable = new ColorDrawable(context.getResources().getColor(
+                        R.color.image_placeholder));
+            }
+            view.setImageDrawable(sDrawable);
+        }
+    }
+
+    public static final DefaultImageProvider DEFAULT_AVATER = new AvatarDefaultImageProvider();
+
+    public static final DefaultImageProvider DEFAULT_BLANK = new BlankDefaultImageProvider();
+
     /**
      * Requests the singleton instance of {@link AccountTypeManager} with data bound from
      * the available authenticators. This method can safely be called from the UI thread.
@@ -91,14 +122,32 @@
      * it is displayed immediately.  Otherwise a request is sent to load the photo
      * from the database.
      */
-    public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme);
+    public abstract void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider);
+
+    /**
+     * Calls {@link #loadPhoto(ImageView, long, boolean, boolean, DefaultImageProvider)} with
+     * {@link #DEFAULT_AVATER}.
+     */
+    public final void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
+        loadPhoto(view, photoId, hires, darkTheme, DEFAULT_AVATER);
+    }
 
     /**
      * Load photo into the supplied image view.  If the photo is already cached,
      * it is displayed immediately.  Otherwise a request is sent to load the photo
      * from the location specified by the URI.
      */
-    public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme);
+    public abstract void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider);
+
+    /**
+     * Calls {@link #loadPhoto(ImageView, Uri, boolean, boolean, DefaultImageProvider)} with
+     * {@link #DEFAULT_AVATER}.
+     */
+    public final void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
+        loadPhoto(view, photoUri, hires, darkTheme, DEFAULT_AVATER);
+    }
 
     /**
      * Remove photo from the supplied image view. This also cancels current pending load request
@@ -236,24 +285,28 @@
     }
 
     @Override
-    public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
+    public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider) {
         if (photoId == 0) {
             // No photo is needed
-            view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+            defaultProvider.applyDefaultImage(view, hires, darkTheme);
             mPendingRequests.remove(view);
         } else {
-            loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme));
+            loadPhotoByIdOrUri(view, Request.createFromId(photoId, hires, darkTheme,
+                    defaultProvider));
         }
     }
 
     @Override
-    public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
+    public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider) {
         if (photoUri == null) {
             // No photo is needed
-            view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+            defaultProvider.applyDefaultImage(view, hires, darkTheme);
             mPendingRequests.remove(view);
         } else {
-            loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme));
+            loadPhotoByIdOrUri(view, Request.createFromUri(photoUri, hires, darkTheme,
+                    defaultProvider));
         }
     }
 
@@ -292,12 +345,12 @@
         BitmapHolder holder = mBitmapHolderCache.get(request.getKey());
         if (holder == null) {
             // The bitmap has not been loaded - should display the placeholder image.
-            view.setImageResource(getDefaultAvatarResId(request.isHires(), request.isDarkTheme()));
+            request.applyDefaultImage(view);
             return false;
         }
 
         if (holder.bytes == null) {
-            view.setImageResource(getDefaultAvatarResId(request.isHires(), request.isDarkTheme()));
+            request.applyDefaultImage(view);
             return holder.fresh;
         }
 
@@ -791,20 +844,25 @@
         private final Uri mUri;
         private final boolean mDarkTheme;
         private final boolean mHires;
+        private final DefaultImageProvider mDefaultProvider;
 
-        private Request(long id, Uri uri, boolean hires, boolean darkTheme) {
+        private Request(long id, Uri uri, boolean hires, boolean darkTheme,
+                DefaultImageProvider defaultProvider) {
             mId = id;
             mUri = uri;
             mDarkTheme = darkTheme;
             mHires = hires;
+            mDefaultProvider = defaultProvider;
         }
 
-        public static Request createFromId(long id, boolean hires, boolean darkTheme) {
-            return new Request(id, null /* no URI */, hires, darkTheme);
+        public static Request createFromId(long id, boolean hires, boolean darkTheme,
+                DefaultImageProvider defaultProvider) {
+            return new Request(id, null /* no URI */, hires, darkTheme, defaultProvider);
         }
 
-        public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme) {
-            return new Request(0 /* no ID */, uri, hires, darkTheme);
+        public static Request createFromUri(Uri uri, boolean hires, boolean darkTheme,
+                DefaultImageProvider defaultProvider) {
+            return new Request(0 /* no ID */, uri, hires, darkTheme, defaultProvider);
         }
 
         public boolean isDarkTheme() {
@@ -841,5 +899,9 @@
         public Object getKey() {
             return mUri == null ? mId : mUri;
         }
+
+        public void applyDefaultImage(ImageView view) {
+            mDefaultProvider.applyDefaultImage(view, mHires, mDarkTheme);
+        }
     }
 }
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 4de62b6..9a3f2ef 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts;
 
+import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountWithDataSet;
 import com.android.contacts.test.NeededForTesting;
@@ -172,11 +173,15 @@
         return detector.detectCountry().getCountryIso();
     }
 
-    public static boolean areAccountsAvailable(Context context) {
+    public static boolean areContactWritableAccountsAvailable(Context context) {
         final List<AccountWithDataSet> accounts =
                 AccountTypeManager.getInstance(context).getAccounts(true /* writeable */);
         return !accounts.isEmpty();
     }
 
-
+    public static boolean areGroupWritableAccountsAvailable(Context context) {
+        final List<AccountWithDataSet> accounts =
+                AccountTypeManager.getInstance(context).getGroupWritableAccounts();
+        return !accounts.isEmpty();
+    }
 }
diff --git a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
new file mode 100644
index 0000000..3e2a893
--- /dev/null
+++ b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
@@ -0,0 +1,192 @@
+/*
+ * 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.activities;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.ContactsContract.Intents;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.contacts.R;
+import com.android.contacts.editor.ContactEditorUtils;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.util.AccountsListAdapter;
+import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
+
+import java.util.List;
+
+/**
+ * This activity can be shown to the user when creating a new contact to inform the user about
+ * which account the contact will be saved in. There is also an option to add an account at
+ * this time. The {@link Intent} in the activity result will contain an extra
+ * {@link #Intents.Insert.ACCOUNT} that contains the {@link AccountWithDataSet} to create
+ * the new contact in. If the activity result doesn't contain intent data, then there is no
+ * account for this contact.
+ */
+public class ContactEditorAccountsChangedActivity extends Activity {
+
+    private static final String TAG = ContactEditorAccountsChangedActivity.class.getSimpleName();
+
+    private static final int SUBACTIVITY_ADD_NEW_ACCOUNT = 1;
+
+    private AccountsListAdapter mAccountListAdapter;
+    private ContactEditorUtils mEditorUtils;
+
+    private final OnItemClickListener mAccountListItemClickListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            if (mAccountListAdapter == null) {
+                return;
+            }
+            saveAccountAndReturnResult(mAccountListAdapter.getItem(position));
+        }
+    };
+
+    private final OnClickListener mAddAccountClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            startActivityForResult(mEditorUtils.createAddWritableAccountIntent(),
+                    SUBACTIVITY_ADD_NEW_ACCOUNT);
+        }
+    };
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        mEditorUtils = ContactEditorUtils.getInstance(this);
+        final List<AccountWithDataSet> accounts = AccountTypeManager.getInstance(this).
+                getAccounts(true);
+        final int numAccounts = accounts.size();
+        if (numAccounts < 0) {
+            throw new IllegalStateException("Cannot have a negative number of accounts");
+        }
+
+        if (numAccounts >= 2) {
+            // When the user has 2+ writable accounts, show a list of accounts so the user can pick
+            // which account to create a contact in.
+            setContentView(R.layout.contact_editor_accounts_changed_activity_with_picker);
+
+            final TextView textView = (TextView) findViewById(R.id.text);
+            textView.setText(getString(R.string.contact_editor_prompt_multiple_accounts));
+
+            final Button button = (Button) findViewById(R.id.add_account_button);
+            button.setText(getString(R.string.add_new_account));
+            button.setOnClickListener(mAddAccountClickListener);
+
+            final ListView accountListView = (ListView) findViewById(R.id.account_list);
+            mAccountListAdapter = new AccountsListAdapter(this,
+                    AccountListFilter.ACCOUNTS_CONTACT_WRITABLE);
+            accountListView.setAdapter(mAccountListAdapter);
+            accountListView.setOnItemClickListener(mAccountListItemClickListener);
+        } else if (numAccounts == 1) {
+            // If the user has 1 writable account we will just show the user a message with 2
+            // possible action buttons.
+            setContentView(R.layout.contact_editor_accounts_changed_activity_with_text);
+
+            final TextView textView = (TextView) findViewById(R.id.text);
+            final Button leftButton = (Button) findViewById(R.id.left_button);
+            final Button rightButton = (Button) findViewById(R.id.right_button);
+
+            final AccountWithDataSet account = accounts.get(0);
+            textView.setText(getString(R.string.contact_editor_prompt_one_account,
+                    account.name));
+
+            // This button allows the user to add a new account to the device and return to
+            // this app afterwards.
+            leftButton.setText(getString(R.string.add_new_account));
+            leftButton.setOnClickListener(mAddAccountClickListener);
+
+            // This button allows the user to continue creating the contact in the specified
+            // account.
+            rightButton.setText(getString(android.R.string.ok));
+            rightButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    saveAccountAndReturnResult(account);
+                }
+            });
+        } else {
+            // If the user has 0 writable accounts, we will just show the user a message with 2
+            // possible action buttons.
+            setContentView(R.layout.contact_editor_accounts_changed_activity_with_text);
+
+            final TextView textView = (TextView) findViewById(R.id.text);
+            final Button leftButton = (Button) findViewById(R.id.left_button);
+            final Button rightButton = (Button) findViewById(R.id.right_button);
+
+            textView.setText(getString(R.string.contact_editor_prompt_zero_accounts));
+
+            // This button allows the user to continue editing the contact as a phone-only
+            // local contact.
+            leftButton.setText(getString(R.string.keep_local));
+            leftButton.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    // Remember that the user wants to create local contacts, so the user is not
+                    // prompted again with this activity.
+                    mEditorUtils.saveDefaultAndAllAccounts(null);
+                    setResult(RESULT_OK);
+                    finish();
+                }
+            });
+
+            // This button allows the user to add a new account to the device and return to
+            // this app afterwards.
+            rightButton.setText(getString(R.string.add_account));
+            rightButton.setOnClickListener(mAddAccountClickListener);
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == SUBACTIVITY_ADD_NEW_ACCOUNT) {
+            // If the user canceled the account setup process, then keep this activity visible to
+            // the user.
+            if (resultCode != RESULT_OK) {
+                return;
+            }
+            // Subactivity was successful, so pass the result back and finish the activity.
+            AccountWithDataSet account = mEditorUtils.getCreatedAccount(resultCode, data);
+            if (account == null) {
+                setResult(resultCode);
+                finish();
+                return;
+            }
+            saveAccountAndReturnResult(account);
+        }
+    }
+
+    private void saveAccountAndReturnResult(AccountWithDataSet account) {
+        // Save this as the default account
+        mEditorUtils.saveDefaultAndAllAccounts(account);
+
+        // Pass account info in activity result intent
+        Intent intent = new Intent();
+        intent.putExtra(Intents.Insert.ACCOUNT, account);
+        setResult(RESULT_OK, intent);
+        finish();
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 50d6f17..baa4b4b 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -84,6 +84,13 @@
             "com.android.phone.CallFeaturesSetting";
 
     /**
+     * Copied from PhoneApp. See comments in Phone app for more detail.
+     */
+    public static final String EXTRA_CALL_ORIGIN = "com.android.phone.CALL_ORIGIN";
+    public static final String CALL_ORIGIN_DIALTACTS =
+            "com.android.contacts.activities.DialtactsActivity";
+
+    /**
      * Just for backward compatibility. Should behave as same as {@link Intent#ACTION_DIAL}.
      */
     private static final String ACTION_TOUCH_DIALER = "com.android.phone.action.TOUCH_DIALER";
@@ -697,7 +704,8 @@
         @Override
         public void onContactSelected(Uri contactUri) {
             PhoneNumberInteraction.startInteractionForPhoneCall(
-                    DialtactsActivity.this, contactUri);
+                    DialtactsActivity.this, contactUri,
+                    CALL_ORIGIN_DIALTACTS);
         }
     };
 
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 3430109..b088b89 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -56,6 +56,7 @@
 import com.android.contacts.util.AccountPromptUtils;
 import com.android.contacts.util.AccountSelectionUtil;
 import com.android.contacts.util.AccountsListAdapter;
+import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.contacts.util.Constants;
 import com.android.contacts.util.DialogManager;
 import com.android.contacts.util.PhoneCapabilityTester;
@@ -200,10 +201,13 @@
         return mProviderStatus == ProviderStatus.STATUS_NORMAL;
     }
 
-    private boolean areAccountsAvailable() {
-        return ContactsUtils.areAccountsAvailable(this);
+    private boolean areContactWritableAccountsAvailable() {
+        return ContactsUtils.areContactWritableAccountsAvailable(this);
     }
 
+    private boolean areGroupWritableAccountsAvailable() {
+        return ContactsUtils.areGroupWritableAccountsAvailable(this);
+    }
 
     /**
      * Initialize fragments that are (or may not be) in the layout.
@@ -604,7 +608,7 @@
             invalidateOptionsMenu();
             showEmptyStateForTab(tab);
             if (tab == TabState.GROUPS) {
-                mGroupsFragment.setAddAccountsVisibility(!areAccountsAvailable());
+                mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
             }
             return;
         }
@@ -625,7 +629,7 @@
                 mFavoritesView.setVisibility(View.GONE);
                 mBrowserView.setVisibility(View.VISIBLE);
                 mDetailsView.setVisibility(View.VISIBLE);
-                mGroupsFragment.setAddAccountsVisibility(!areAccountsAvailable());
+                mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
                 break;
             case ALL:
                 mFavoritesView.setVisibility(View.GONE);
@@ -686,7 +690,7 @@
                     break;
                 case GROUPS:
                     mContactsUnavailableFragment.setMessageText(R.string.noGroups,
-                            areAccountsAvailable() ? -1 : R.string.noAccounts);
+                            areGroupWritableAccountsAvailable() ? -1 : R.string.noAccounts);
                     break;
                 case ALL:
                     mContactsUnavailableFragment.setMessageText(R.string.noContacts, -1);
@@ -712,7 +716,7 @@
                 mActionBarAdapter.setCurrentTab(selectedTab, false);
                 showEmptyStateForTab(selectedTab);
                 if (selectedTab == TabState.GROUPS) {
-                    mGroupsFragment.setAddAccountsVisibility(!areAccountsAvailable());
+                    mGroupsFragment.setAddAccountsVisibility(!areGroupWritableAccountsAvailable());
                 }
                 invalidateOptionsMenu();
             }
@@ -922,7 +926,8 @@
             // If there are no accounts on the device and we should show the "no account" prompt
             // (based on {@link SharedPreferences}), then launch the account setup activity so the
             // user can sign-in or create an account.
-            if (!areAccountsAvailable() && AccountPromptUtils.shouldShowAccountPrompt(this)) {
+            if (!areContactWritableAccountsAvailable() &&
+                    AccountPromptUtils.shouldShowAccountPrompt(this)) {
                 AccountPromptUtils.launchAccountPrompt(this);
                 return;
             }
@@ -1304,7 +1309,7 @@
                     break;
                 case GROUPS:
                     // Do not display the "new group" button if no accounts are available
-                    if (areAccountsAvailable()) {
+                    if (areGroupWritableAccountsAvailable()) {
                         addGroupMenu.setVisible(true);
                     } else {
                         addGroupMenu.setVisible(false);
@@ -1410,7 +1415,8 @@
         popup.setAnchorView(mAddGroupImageView);
         // Create a list adapter with all writeable accounts (assume that the writeable accounts all
         // allow group creation).
-        final AccountsListAdapter adapter = new AccountsListAdapter(this, true);
+        final AccountsListAdapter adapter = new AccountsListAdapter(this,
+                AccountListFilter.ACCOUNTS_GROUP_WRITABLE);
         popup.setAdapter(adapter);
         popup.setOnItemClickListener(new OnItemClickListener() {
             @Override
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 2d93a98..0628db4 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -337,7 +337,15 @@
 
     @Override
     public void onVisibilityChanged(boolean visible) {
-        mShowOptionsMenu = visible;
+        if (mShowOptionsMenu != visible) {
+            mShowOptionsMenu = visible;
+            // Invalidate the options menu since we are changing the list of options shown in it.
+            Activity activity = getActivity();
+            if (activity != null) {
+                activity.invalidateOptionsMenu();
+            }
+        }
+
         if (visible && isResumed()) {
             refreshData();
         }
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index bdcd6b0..b81cebf 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -249,7 +249,8 @@
         setDataOrHideIfNone(snippet, statusView);
         if (photoUri != null) {
             ContactPhotoManager.getInstance(context).loadPhoto(
-                    statusPhotoView, Uri.parse(photoUri), true, false);
+                    statusPhotoView, Uri.parse(photoUri), true, false,
+                    ContactPhotoManager.DEFAULT_BLANK);
             statusPhotoView.setVisibility(View.VISIBLE);
         } else {
             statusPhotoView.setVisibility(View.GONE);
@@ -339,7 +340,7 @@
             pushLayerView.setEnabled(false);
         }
         contactPhotoManager.loadPhoto(imageView, Uri.parse(streamItemPhoto.getPhotoUri()), true,
-                false);
+                false, ContactPhotoManager.DEFAULT_BLANK);
     }
 
     @VisibleForTesting
@@ -349,15 +350,16 @@
                 R.id.stream_item_attribution);
         TextView commentsView = (TextView) rootView.findViewById(R.id.stream_item_comments);
         ImageGetter imageGetter = new DefaultImageGetter(context.getPackageManager());
-        htmlView.setText(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null));
-        attributionView.setText(ContactBadgeUtil.getSocialDate(streamItem, context));
-        if (streamItem.getComments() != null) {
-            commentsView.setText(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter,
-                    null));
-            commentsView.setVisibility(View.VISIBLE);
-        } else {
-            commentsView.setVisibility(View.GONE);
-        }
+
+        // Stream item text
+        setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getText(), imageGetter, null),
+                htmlView);
+        // Attribution
+        setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(streamItem, context),
+                attributionView);
+        // Comments
+        setDataOrHideIfNone(HtmlUtils.fromHtml(context, streamItem.getComments(), imageGetter,
+                null), commentsView);
         return rootView;
     }
 
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index 45cb03b..d1c5868 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -27,8 +27,12 @@
 import com.android.phone.HapticFeedback;
 
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
 import android.app.Fragment;
 import android.content.Context;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.res.Resources;
 import android.database.Cursor;
@@ -37,16 +41,15 @@
 import android.media.AudioManager;
 import android.media.ToneGenerator;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.provider.Contacts.Intents.Insert;
 import android.provider.Contacts.People;
 import android.provider.Contacts.Phones;
 import android.provider.Contacts.PhonesColumns;
 import android.provider.Settings;
-import android.telephony.PhoneNumberFormattingTextWatcher;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.PhoneStateListener;
 import android.telephony.TelephonyManager;
@@ -61,16 +64,21 @@
 import android.view.MenuInflater;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.View.OnClickListener;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.widget.AdapterView;
 import android.widget.BaseAdapter;
+import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ImageView;
 import android.widget.ListView;
 import android.widget.PopupMenu;
 import android.widget.TextView;
 
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Fragment that displays a twelve-key phone dialpad.
  */
@@ -117,6 +125,11 @@
     private ListView mDialpadChooser;
     private DialpadChooserAdapter mDialpadChooserAdapter;
 
+    /**
+     * Regular expression prohibiting manual phone call. Can be empty, which means "no rule".
+     */
+    private String mProhibitedPhoneNumberRegexp;
+
     private boolean mShowOptionsMenu;
 
     private boolean mHasVoicemail = false;
@@ -219,6 +232,9 @@
         }
 
         setHasOptionsMenu(true);
+
+        mProhibitedPhoneNumberRegexp = getResources().getString(
+                R.string.config_prohibited_phone_number_regexp);
     }
 
     @Override
@@ -794,6 +810,26 @@
         getActivity().finish();
     }
 
+    public static class CallProhibitedDialogFragment extends DialogFragment {
+        public static CallProhibitedDialogFragment newInstance() {
+            return new CallProhibitedDialogFragment();
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            return new AlertDialog.Builder(getActivity())
+                    .setTitle(R.string.phone_call_prohibited)
+                    .setPositiveButton(android.R.string.ok,
+                            new DialogInterface.OnClickListener() {
+                                @Override
+                                public void onClick(DialogInterface dialog, int which) {
+                                    dismiss();
+                                }
+                            })
+                    .create();
+        }
+    }
+
     /**
      * In most cases, when the dial button is pressed, there is a
      * number in digits area. Pack it in the intent, start the
@@ -847,9 +883,31 @@
         } else {
             final String number = mDigits.getText().toString();
 
-            startActivity(newDialNumberIntent(number));
-            mDigits.getText().clear();  // TODO: Fix bug 1745781
-            getActivity().finish();
+            // "persist.radio.otaspdial" is a temporary hack needed for one carrier's automated
+            // test equipment.
+            // TODO: clean it up.
+            if (number != null
+                    && !TextUtils.isEmpty(mProhibitedPhoneNumberRegexp)
+                    && number.matches(mProhibitedPhoneNumberRegexp)
+                    && (SystemProperties.getInt("persist.radio.otaspdial", 0) != 1)) {
+                Log.i(TAG, "The phone number is prohibited explicitly by a rule.");
+                if (getActivity() != null) {
+                    DialogFragment dialogFragment = CallProhibitedDialogFragment.newInstance();
+                    dialogFragment.show(getFragmentManager(), "phone_prohibited_dialog");
+                }
+
+                // Clear the digits just in case.
+                mDigits.getText().clear();
+            } else {
+                final Intent intent = newDialNumberIntent(number);
+                if (getActivity() instanceof DialtactsActivity) {
+                    intent.putExtra(DialtactsActivity.EXTRA_CALL_ORIGIN,
+                            DialtactsActivity.CALL_ORIGIN_DIALTACTS);
+                }
+                startActivity(intent);
+                mDigits.getText().clear();  // TODO: Fix bug 1745781
+                getActivity().finish();
+            }
         }
     }
 
@@ -1224,7 +1282,7 @@
                 // been entered, or if there is a last dialed number
                 // that could be redialed.
                 mDialButton.setEnabled(digitsNotEmpty ||
-                                       !TextUtils.isEmpty(mLastNumberDialed));
+                        !TextUtils.isEmpty(mLastNumberDialed));
             }
         }
         mDelete.setEnabled(digitsNotEmpty);
diff --git a/src/com/android/contacts/dialpad/DigitsEditText.java b/src/com/android/contacts/dialpad/DigitsEditText.java
index 753afd4..68335da 100644
--- a/src/com/android/contacts/dialpad/DigitsEditText.java
+++ b/src/com/android/contacts/dialpad/DigitsEditText.java
@@ -59,9 +59,22 @@
     @Override
     public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
         if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
-            // AsYouTypeFormatter frequently replaces digits with formatted ones, which makes
-            // tts too verbose. Let's ignore the whole event.
-            return;
+            // Since we're replacing the text every time we add or remove a
+            // character, only read the difference. (issue 5337550)
+            final int added = event.getAddedCount();
+            final int removed = event.getRemovedCount();
+            final int length = event.getBeforeText().length();
+            if (added > removed) {
+                event.setRemovedCount(0);
+                event.setAddedCount(1);
+                event.setFromIndex(length);
+            } else if (removed > added) {
+                event.setRemovedCount(1);
+                event.setAddedCount(0);
+                event.setFromIndex(length - 1);
+            } else {
+                return;
+            }
         } else if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) {
             // The parent EditText class lets tts read "edit box" when this View has a focus, which
             // confuses users on app launch (issue 5275935).
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index c09d8cd..f7e0c23 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -20,6 +20,7 @@
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.GroupMetaDataLoader;
 import com.android.contacts.R;
+import com.android.contacts.activities.ContactEditorAccountsChangedActivity;
 import com.android.contacts.activities.ContactEditorActivity;
 import com.android.contacts.activities.JoinContactActivity;
 import com.android.contacts.editor.AggregationSuggestionEngine.Suggestion;
@@ -33,6 +34,7 @@
 import com.android.contacts.model.EntityModifier;
 import com.android.contacts.model.GoogleAccountType;
 import com.android.contacts.util.AccountsListAdapter;
+import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 
 import android.accounts.Account;
 import android.app.Activity;
@@ -192,6 +194,7 @@
     private static final int REQUEST_CODE_JOIN = 0;
     private static final int REQUEST_CODE_CAMERA_WITH_DATA = 1;
     private static final int REQUEST_CODE_PHOTO_PICKED_WITH_DATA = 2;
+    private static final int REQUEST_CODE_ACCOUNTS_CHANGED = 3;
 
     private Bitmap mPhoto = null;
     private long mRawContactIdRequestingPhoto = -1;
@@ -218,6 +221,8 @@
     private long mContactIdForJoin;
     private boolean mContactWritableForJoin;
 
+    private ContactEditorUtils mEditorUtils;
+
     private LinearLayout mContent;
     private EntityDeltaList mState;
 
@@ -316,6 +321,7 @@
     public void onAttach(Activity activity) {
         super.onAttach(activity);
         mContext = activity;
+        mEditorUtils = ContactEditorUtils.getInstance(mContext);
         loadPhotoPickSize();
     }
 
@@ -370,7 +376,7 @@
                 } else {
                     // No Account specified. Let the user choose
                     // Load Accounts async so that we can present them
-                    createContact();
+                    selectAccountAndCreateContact();
                 }
             } else if (ContactEditorActivity.ACTION_SAVE_COMPLETED.equals(mAction)) {
                 // do nothing
@@ -528,17 +534,43 @@
         }
     }
 
+    private void selectAccountAndCreateContact() {
+        // If this is a local profile, then skip the logic about showing the accounts changed
+        // activity and create a phone-local contact.
+        if (mNewLocalProfile) {
+            createContact(null);
+            return;
+        }
+
+        // If there is no default account or the accounts have changed such that we need to
+        // prompt the user again, then launch the account prompt.
+        if (mEditorUtils.shouldShowAccountChangedNotification()) {
+            Intent intent = new Intent(mContext, ContactEditorAccountsChangedActivity.class);
+            mStatus = Status.SUB_ACTIVITY;
+            startActivityForResult(intent, REQUEST_CODE_ACCOUNTS_CHANGED);
+        } else {
+            // Otherwise, there should be a default account. Then either create a local contact
+            // (if default account is null) or create a contact with the specified account.
+            AccountWithDataSet defaultAccount = mEditorUtils.getDefaultAccount();
+            if (defaultAccount == null) {
+                createContact(null);
+            } else {
+                createContact(defaultAccount);
+            }
+        }
+    }
+
     /**
-     * Shows the account creation screen. An account associated with the contact is automatically
-     * selected. If there's no available account, device-local contact should be created.
+     * Create a contact by automatically selecting the first account. If there's no available
+     * account, a device-local contact should be created.
      */
     private void createContact() {
         final List<AccountWithDataSet> accounts =
                 AccountTypeManager.getInstance(mContext).getAccounts(true);
-        // No Accounts available or creating a local profile.  Create a phone-local contact.
-        if (accounts.isEmpty() || mNewLocalProfile) {
+        // No Accounts available. Create a phone-local contact.
+        if (accounts.isEmpty()) {
             createContact(null);
-            return;  // Don't show a dialog.
+            return;
         }
 
         // We have an account switcher in "create-account" screen, so don't need to ask a user to
@@ -546,7 +578,6 @@
         createContact(accounts.get(0));
     }
 
-
     /**
      * Shows account creation screen associated with a given account.
      *
@@ -760,6 +791,28 @@
         }
     }
 
+    private void saveDefaultAccountIfNecessary() {
+        // Verify that this is a newly created contact, that the contact is composed of only
+        // 1 raw contact, and that the contact is not a user profile.
+        if (!Intent.ACTION_INSERT.equals(mAction) && mState.size() == 1 &&
+                !isEditingUserProfile()) {
+            return;
+        }
+
+        // Find the associated account for this contact (retrieve it here because there are
+        // multiple paths to creating a contact and this ensures we always have the correct
+        // account).
+        final EntityDelta entity = mState.get(0);
+        final ValuesDelta values = entity.getValues();
+        String name = values.getAsString(RawContacts.ACCOUNT_NAME);
+        String type = values.getAsString(RawContacts.ACCOUNT_TYPE);
+        String dataSet = values.getAsString(RawContacts.DATA_SET);
+
+        AccountWithDataSet account = (name == null || type == null) ? null :
+                new AccountWithDataSet(name, type, dataSet);
+        mEditorUtils.saveDefaultAndAllAccounts(account);
+    }
+
     private void addAccountSwitcher(
             final EntityDelta currentState, BaseRawContactEditorView editor) {
         ValuesDelta values = currentState.getValues();
@@ -774,7 +827,8 @@
             public void onClick(View v) {
                 final ListPopupWindow popup = new ListPopupWindow(mContext, null);
                 final AccountsListAdapter adapter =
-                        new AccountsListAdapter(mContext, true, currentAccount);
+                        new AccountsListAdapter(mContext,
+                        AccountListFilter.ACCOUNTS_CONTACT_WRITABLE, currentAccount);
                 popup.setWidth(anchorView.getWidth());
                 popup.setAnchorView(anchorView);
                 popup.setAdapter(adapter);
@@ -985,6 +1039,10 @@
 
         setEnabled(false);
 
+        // Store account as default account, only if this is a new contact
+        saveDefaultAccountIfNecessary();
+
+        // Save contact
         Intent intent = ContactSaveService.createSaveContactIntent(getActivity(), mState,
                 SAVE_MODE_EXTRA_KEY, saveMode, isEditingUserProfile(),
                 getActivity().getClass(), ContactEditorActivity.ACTION_SAVE_COMPLETED);
@@ -1536,10 +1594,10 @@
             mStatus = Status.EDITING;
         }
 
-        // Ignore failed requests
-        if (resultCode != Activity.RESULT_OK) return;
         switch (requestCode) {
             case REQUEST_CODE_PHOTO_PICKED_WITH_DATA: {
+                // Ignore failed requests
+                if (resultCode != Activity.RESULT_OK) return;
                 // As we are coming back to this view, the editor will be reloaded automatically,
                 // which will cause the photo that is set here to disappear. To prevent this,
                 // we remember to set a flag which is interpreted after loading.
@@ -1552,16 +1610,40 @@
                 break;
             }
             case REQUEST_CODE_CAMERA_WITH_DATA: {
+                // Ignore failed requests
+                if (resultCode != Activity.RESULT_OK) return;
                 doCropPhoto(mCurrentPhotoFile);
                 break;
             }
             case REQUEST_CODE_JOIN: {
+                // Ignore failed requests
+                if (resultCode != Activity.RESULT_OK) return;
                 if (data != null) {
                     final long contactId = ContentUris.parseId(data.getData());
                     joinAggregate(contactId);
                 }
                 break;
             }
+            case REQUEST_CODE_ACCOUNTS_CHANGED: {
+                // Bail if the account selector was not successful.
+                if (resultCode != Activity.RESULT_OK) {
+                    mListener.onReverted();
+                    return;
+                }
+                // If there's an account specified, use it.
+                if (data != null) {
+                    AccountWithDataSet account = data.getParcelableExtra(Intents.Insert.ACCOUNT);
+                    if (account != null) {
+                        createContact(account);
+                        return;
+                    }
+                }
+                // If there isn't an account specified, then this is likely a phone-local
+                // contact, so we should continue setting up the editor by automatically selecting
+                // the most appropriate account.
+                createContact();
+                break;
+            }
         }
     }
 
diff --git a/src/com/android/contacts/editor/ContactEditorUtils.java b/src/com/android/contacts/editor/ContactEditorUtils.java
new file mode 100644
index 0000000..0e27223
--- /dev/null
+++ b/src/com/android/contacts/editor/ContactEditorUtils.java
@@ -0,0 +1,249 @@
+/*
+ * 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.editor;
+
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.test.NeededForTesting;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility methods for the "account changed" notification in the new contact creation flow.
+ *
+ * TODO Remove all the "@VisibleForTesting"s once they're actually used in the app.
+ *      (Until then we need them to avoid "no such method" in tests)
+ */
+public class ContactEditorUtils {
+    private static final String TAG = "ContactEditorUtils";
+
+    private static final String KEY_DEFAULT_ACCOUNT = "ContactEditorUtils_default_account";
+    private static final String KEY_KNOWN_ACCOUNTS = "ContactEditorUtils_known_accounts";
+    // Key to tell the first time launch.
+    private static final String KEY_ANYTHING_SAVED = "ContactEditorUtils_anything_saved";
+
+    private static final List<AccountWithDataSet> EMPTY_ACCOUNTS = ImmutableList.of();
+
+    private static ContactEditorUtils sInstance;
+
+    private final Context mContext;
+    private final SharedPreferences mPrefs;
+    private final AccountTypeManager mAccountTypes;
+
+    private ContactEditorUtils(Context context) {
+        this(context, AccountTypeManager.getInstance(context));
+    }
+
+    @VisibleForTesting
+    ContactEditorUtils(Context context, AccountTypeManager accountTypes) {
+        mContext = context.getApplicationContext();
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+        mAccountTypes = accountTypes;
+    }
+
+    public static synchronized ContactEditorUtils getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new ContactEditorUtils(context);
+        }
+        return sInstance;
+    }
+
+    void cleanupForTest() {
+        mPrefs.edit().remove(KEY_DEFAULT_ACCOUNT).remove(KEY_KNOWN_ACCOUNTS)
+                .remove(KEY_ANYTHING_SAVED).apply();
+    }
+
+    private List<AccountWithDataSet> getWritableAccounts() {
+        return mAccountTypes.getAccounts(true);
+    }
+
+    /**
+     * @return true if it's the first launch and {@link #saveDefaultAndAllAccounts} has never
+     *     been called.
+     */
+    private boolean isFirstLaunch() {
+        return !mPrefs.getBoolean(KEY_ANYTHING_SAVED, false);
+    }
+
+    /**
+     * Saves all writable accounts and the default account, which can later be obtained
+     * with {@link #getDefaultAccount}.
+     *
+     * This should be called when saving a newly created contact.
+     *
+     * @param defaultAccount the account used to save a newly created contact.  Or pass {@code null}
+     *     If the user selected "local only".
+     */
+    @NeededForTesting
+    public void saveDefaultAndAllAccounts(AccountWithDataSet defaultAccount) {
+        mPrefs.edit()
+                .putBoolean(KEY_ANYTHING_SAVED, true)
+                .putString(
+                        KEY_KNOWN_ACCOUNTS,AccountWithDataSet.stringifyList(getWritableAccounts()))
+                .putString(KEY_DEFAULT_ACCOUNT,
+                        (defaultAccount == null) ? "" : defaultAccount.stringify())
+                .apply();
+    }
+
+    /**
+     * @return the default account saved with {@link #saveDefaultAndAllAccounts}.
+     *
+     * Note the {@code null} return value can mean either {@link #saveDefaultAndAllAccounts} has
+     * never been called, or {@code null} was passed to {@link #saveDefaultAndAllAccounts} --
+     * i.e. the user selected "local only".
+     *
+     * Also note that the returned account may have been removed already.
+     */
+    @NeededForTesting
+    public AccountWithDataSet getDefaultAccount() {
+        final String saved = mPrefs.getString(KEY_DEFAULT_ACCOUNT, null);
+        if (TextUtils.isEmpty(saved)) {
+            return null;
+        }
+        return AccountWithDataSet.unstringify(saved);
+    }
+
+    /**
+     * @return true if an account still exists.  {@code null} is considered "local only" here,
+     *    so it's valid too.
+     */
+    @VisibleForTesting
+    boolean isValidAccount(AccountWithDataSet account) {
+        if (account == null) {
+            return true; // It's "local only" account, which is valid.
+        }
+        return getWritableAccounts().contains(account);
+    }
+
+    /**
+     * @return saved known accounts, or an empty list if none has been saved yet.
+     */
+    @VisibleForTesting
+    List<AccountWithDataSet> getSavedAccounts() {
+        final String saved = mPrefs.getString(KEY_KNOWN_ACCOUNTS, null);
+        if (TextUtils.isEmpty(saved)) {
+            return EMPTY_ACCOUNTS;
+        }
+        return AccountWithDataSet.unstringifyList(saved);
+    }
+
+    /**
+     * @return true if the contact editor should show the "accounts changed" notification, that is:
+     * - If it's the first launch.
+     * - Or, if an account has been added.
+     * - Or, if the default account has been removed.
+     *
+     * Note if this method returns {@code false}, the caller can safely assume that
+     * {@link #getDefaultAccount} will return a valid account.  (Either an account which still
+     * exists, or {@code null} which should be interpreted as "local only".)
+     */
+    @NeededForTesting
+    public boolean shouldShowAccountChangedNotification() {
+        if (isFirstLaunch()) {
+            return true;
+        }
+
+        // Account added?
+        final List<AccountWithDataSet> savedAccounts = getSavedAccounts();
+        for (AccountWithDataSet account : getWritableAccounts()) {
+            if (!savedAccounts.contains(account)) {
+                return true; // New account found.
+            }
+        }
+
+        // Does default account still exist?
+        if (!isValidAccount(getDefaultAccount())) {
+            return true;
+        }
+
+        // All good.
+        return false;
+    }
+
+    @VisibleForTesting
+    String[] getWritableAccountTypeStrings() {
+        final Set<String> types = Sets.newHashSet();
+        for (AccountType type : mAccountTypes.getAccountTypes(true)) {
+            types.add(type.accountType);
+        }
+        return types.toArray(new String[types.size()]);
+    }
+
+    /**
+     * Create an {@link Intent} to start "add new account" setup wizard.  Selectable account
+     * types will be limited to ones that supports editing contacts.
+     *
+     * Use {@link Activity#startActivityForResult} or
+     * {@link android.app.Fragment#startActivityForResult} to start the wizard, and
+     * {@link Activity#onActivityResult} or {@link android.app.Fragment#onActivityResult} to
+     * get the result.
+     */
+    @NeededForTesting
+    public Intent createAddWritableAccountIntent() {
+        return AccountManager.newChooseAccountIntent(
+                null, // selectedAccount
+                new ArrayList<Account>(), // allowableAccounts
+                getWritableAccountTypeStrings(), // allowableAccountTypes
+                false, // alwaysPromptForAccount
+                null, // descriptionOverrideText
+                null, // addAccountAuthTokenType
+                null, // addAccountRequiredFeatures
+                null // addAccountOptions
+                );
+    }
+
+    /**
+     * Parses a result from {@link #createAddWritableAccountIntent} and returns the created
+     * {@link Account}, or null if the user has canceled the wizard.  Pass the {@code resultCode}
+     * and {@code data} parameters passed to {@link Activity#onActivityResult} or
+     * {@link android.app.Fragment#onActivityResult}.
+     *
+     * Note although the return type is {@link AccountWithDataSet}, return values from this method
+     * will never have {@link AccountWithDataSet#dataSet} set, as there's no way to create an
+     * extension package account from setup wizard.
+     */
+    @NeededForTesting
+    public AccountWithDataSet getCreatedAccount(int resultCode, Intent resultData) {
+        // Javadoc doesn't say anything about resultCode but that the data intent will be non null
+        // on success.
+        if (resultData == null) return null;
+
+        final String accountType = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE);
+        final String accountName = resultData.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
+
+        // Just in case
+        if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) return null;
+
+        return new AccountWithDataSet(accountName, accountType, null);
+    }
+}
+
diff --git a/src/com/android/contacts/editor/SelectAccountDialogFragment.java b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
index 9dbe20a..3a8681a 100644
--- a/src/com/android/contacts/editor/SelectAccountDialogFragment.java
+++ b/src/com/android/contacts/editor/SelectAccountDialogFragment.java
@@ -19,6 +19,7 @@
 import com.android.contacts.R;
 import com.android.contacts.model.AccountWithDataSet;
 import com.android.contacts.util.AccountsListAdapter;
+import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 
 import android.app.AlertDialog;
 import android.app.Dialog;
@@ -37,13 +38,18 @@
 public class SelectAccountDialogFragment extends DialogFragment {
     public static final String TAG = "SelectAccountDialogFragment";
 
-    private int mTitleResourceId = R.string.dialog_new_contact_account;
+    // TODO: This dialog is used in the context of group editing by default, but should be generic
+    // to work for contact editing as well. Save/restore the resource ID and account list filter
+    // that are passed in as parameters on device rotation. Bug: 5369853
+    private int mTitleResourceId = R.string.dialog_new_group_account;
+    private AccountListFilter mAccountListFilter = AccountListFilter.ACCOUNTS_GROUP_WRITABLE;
 
     public SelectAccountDialogFragment() {
     }
 
-    public SelectAccountDialogFragment(int titleResourceId) {
+    public SelectAccountDialogFragment(int titleResourceId, AccountListFilter accountListFilter) {
         mTitleResourceId = titleResourceId;
+        mAccountListFilter = accountListFilter;
     }
 
     @Override
@@ -51,7 +57,7 @@
         final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
 
         final AccountsListAdapter accountAdapter = new AccountsListAdapter(builder.getContext(),
-                true);
+                mAccountListFilter);
 
         final DialogInterface.OnClickListener clickListener =
                 new DialogInterface.OnClickListener() {
diff --git a/src/com/android/contacts/group/GroupBrowseListFragment.java b/src/com/android/contacts/group/GroupBrowseListFragment.java
index 82539e8..79bdd09 100644
--- a/src/com/android/contacts/group/GroupBrowseListFragment.java
+++ b/src/com/android/contacts/group/GroupBrowseListFragment.java
@@ -144,7 +144,7 @@
                 startActivity(intent);
             }
         });
-        setAddAccountsVisibility(!ContactsUtils.areAccountsAvailable(mContext));
+        setAddAccountsVisibility(!ContactsUtils.areGroupWritableAccountsAvailable(mContext));
 
         return mRootView;
     }
@@ -214,7 +214,7 @@
 
     private void bindGroupList() {
         mEmptyView.setText(R.string.noGroups);
-        setAddAccountsVisibility(!ContactsUtils.areAccountsAvailable(mContext));
+        setAddAccountsVisibility(!ContactsUtils.areGroupWritableAccountsAvailable(mContext));
         if (mGroupListCursor == null) {
             return;
         }
diff --git a/src/com/android/contacts/group/GroupDetailDisplayUtils.java b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
index bb4cd5c..da5e0e9 100644
--- a/src/com/android/contacts/group/GroupDetailDisplayUtils.java
+++ b/src/com/android/contacts/group/GroupDetailDisplayUtils.java
@@ -24,6 +24,7 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.ImageView;
+import android.widget.TextView;
 
 public class GroupDetailDisplayUtils {
 
@@ -39,13 +40,21 @@
 
     public static void bindGroupSourceView(Context context, View view, String accountTypeString,
             String dataSet) {
-        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, dataSet);
+
+        TextView label = (TextView) view.findViewById(android.R.id.title);
+        if (label == null) {
+            throw new IllegalStateException("Group source view must contain a TextView with id"
+                    + "android.R.id.label");
+        }
+        label.setText(accountType.getViewGroupLabel(context));
+
+        ImageView accountIcon = (ImageView) view.findViewById(android.R.id.icon);
+        if (accountIcon == null) {
+            throw new IllegalStateException("Group source view must contain an ImageView with id"
+                    + "android.R.id.icon");
+        }
         accountIcon.setImageDrawable(accountType.getDisplayIcon(context));
     }
 }
\ No newline at end of file
diff --git a/src/com/android/contacts/group/GroupEditorFragment.java b/src/com/android/contacts/group/GroupEditorFragment.java
index 99e6b48..1d1237e 100644
--- a/src/com/android/contacts/group/GroupEditorFragment.java
+++ b/src/com/android/contacts/group/GroupEditorFragment.java
@@ -28,6 +28,7 @@
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.util.AccountsListAdapter.AccountListFilter;
 import com.android.internal.util.Objects;
 
 import android.accounts.Account;
@@ -332,7 +333,7 @@
 
         mStatus = Status.SELECTING_ACCOUNT;
         final SelectAccountDialogFragment dialog = new SelectAccountDialogFragment(
-                R.string.dialog_new_group_account);
+                R.string.dialog_new_group_account, AccountListFilter.ACCOUNTS_GROUP_WRITABLE);
         dialog.setTargetFragment(this, 0);
         dialog.show(getFragmentManager(), SelectAccountDialogFragment.TAG);
     }
diff --git a/src/com/android/contacts/interactions/PhoneNumberInteraction.java b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
index a42456c..d10ec06 100644
--- a/src/com/android/contacts/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
@@ -21,14 +21,11 @@
 import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
+import com.android.contacts.activities.DialtactsActivity;
 import com.android.contacts.model.AccountType;
 import com.android.contacts.model.AccountType.StringInflater;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.DataKind;
-import com.android.i18n.phonenumbers.NumberParseException;
-import com.android.i18n.phonenumbers.PhoneNumberUtil;
-import com.android.i18n.phonenumbers.PhoneNumberUtil.MatchType;
-import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Activity;
@@ -53,7 +50,6 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.RawContacts;
-import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
@@ -197,17 +193,21 @@
 
         private static final String ARG_PHONE_LIST = "phoneList";
         private static final String ARG_INTERACTION_TYPE = "interactionType";
+        private static final String ARG_CALL_ORIGIN = "callOrigin";
 
         private InteractionType mInteractionType;
         private ListAdapter mPhonesAdapter;
         private List<PhoneItem> mPhoneList;
+        private String mCallOrigin;
 
         public static void show(FragmentManager fragmentManager,
-                ArrayList<PhoneItem> phoneList, InteractionType interactionType) {
+                ArrayList<PhoneItem> phoneList, InteractionType interactionType,
+                String callOrigin) {
             PhoneDisambiguationDialogFragment fragment = new PhoneDisambiguationDialogFragment();
             Bundle bundle = new Bundle();
             bundle.putParcelableArrayList(ARG_PHONE_LIST, phoneList);
             bundle.putSerializable(ARG_INTERACTION_TYPE, interactionType);
+            bundle.putString(ARG_CALL_ORIGIN, callOrigin);
             fragment.setArguments(bundle);
             fragment.show(fragmentManager, TAG);
         }
@@ -218,6 +218,8 @@
             mPhoneList = getArguments().getParcelableArrayList(ARG_PHONE_LIST);
             mInteractionType =
                     (InteractionType) getArguments().getSerializable(ARG_INTERACTION_TYPE);
+            mCallOrigin = getArguments().getString(ARG_CALL_ORIGIN);
+
             mPhonesAdapter = new PhoneItemAdapter(activity, mPhoneList, mInteractionType);
             final LayoutInflater inflater = activity.getLayoutInflater();
             final View setPrimaryView = inflater.inflate(R.layout.set_primary_checkbox, null);
@@ -242,7 +244,7 @@
                 }
 
                 PhoneNumberInteraction.performAction(getActivity(), phoneItem.phoneNumber,
-                        mInteractionType);
+                        mInteractionType, mCallOrigin);
             } else {
                 dialog.dismiss();
             }
@@ -266,22 +268,31 @@
     private final OnDismissListener mDismissListener;
     private final InteractionType mInteractionType;
 
+    private final String mCallOrigin;
+
     private CursorLoader mLoader;
 
     @VisibleForTesting
     /* package */ PhoneNumberInteraction(Context context, InteractionType interactionType,
             DialogInterface.OnDismissListener dismissListener) {
+        this(context, interactionType, dismissListener, null);
+    }
+
+    private PhoneNumberInteraction(Context context, InteractionType interactionType,
+            DialogInterface.OnDismissListener dismissListener, String callOrigin) {
         mContext = context;
         mInteractionType = interactionType;
         mDismissListener = dismissListener;
+        mCallOrigin = callOrigin;
     }
 
     private void performAction(String phoneNumber) {
-        PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType);
+        PhoneNumberInteraction.performAction(mContext, phoneNumber, mInteractionType, mCallOrigin);
     }
 
     private static void performAction(
-            Context context, String phoneNumber, InteractionType interactionType) {
+            Context context, String phoneNumber, InteractionType interactionType,
+            String callOrigin) {
         Intent intent;
         switch (interactionType) {
             case SMS:
@@ -291,6 +302,9 @@
             default:
                 intent = new Intent(
                         Intent.ACTION_CALL_PRIVILEGED, Uri.fromParts("tel", phoneNumber, null));
+                if (callOrigin != null) {
+                    intent.putExtra(DialtactsActivity.EXTRA_CALL_ORIGIN, callOrigin);
+                }
                 break;
         }
         context.startActivity(intent);
@@ -402,6 +416,17 @@
     }
 
     /**
+     * @param callOrigin If non null, {@link DialtactsActivity#EXTRA_CALL_ORIGIN} will be
+     * appended to the Intent initiating phone call. See comments in Phone package (PhoneApp)
+     * for more detail.
+     */
+    public static void startInteractionForPhoneCall(Activity activity, Uri uri,
+            String callOrigin) {
+        (new PhoneNumberInteraction(activity, InteractionType.PHONE_CALL, null, callOrigin))
+                .startInteraction(uri);
+    }
+
+    /**
      * Start text messaging (a.k.a SMS) action using given contact Uri. If there are multiple
      * candidates for the phone call, dialog is automatically shown and the user is asked to choose
      * one.
@@ -422,6 +447,6 @@
     @VisibleForTesting
     /* package */ void showDisambiguationDialog(ArrayList<PhoneItem> phoneList) {
         PhoneDisambiguationDialogFragment.show(((Activity)mContext).getFragmentManager(),
-                phoneList, mInteractionType);
+                phoneList, mInteractionType, mCallOrigin);
     }
 }
diff --git a/src/com/android/contacts/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
index 938d43a..015a364 100644
--- a/src/com/android/contacts/list/PhoneNumberPickerFragment.java
+++ b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
@@ -55,6 +55,9 @@
 
     private static final String KEY_FILTER = "filter";
 
+    /** true if the loader has started at least once. */
+    private boolean mLoaderStarted;
+
     // A complete copy from DefaultContactBrowserListFragment
     // TODO: should be able to share logic around filter header.
     private class FilterHeaderClickListener implements OnClickListener {
@@ -204,6 +207,12 @@
     }
 
     @Override
+    protected void startLoading() {
+        mLoaderStarted = true;
+        super.startLoading();
+    }
+
+    @Override
     protected ContactEntryListAdapter createListAdapter() {
         if (!isLegacyCompatibilityMode()) {
             PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity());
@@ -272,7 +281,11 @@
             ContactListFilter.storeToPreferences(mPrefs, mFilter);
         }
 
-        reloadData();
+        // This method can be called before {@link #onStart} where we start the loader.  In that
+        // case we shouldn't start the loader yet, as we haven't done all initialization yet.
+        if (mLoaderStarted) {
+            reloadData();
+        }
         updateFilterHeaderView();
     }
 }
diff --git a/src/com/android/contacts/model/AccountType.java b/src/com/android/contacts/model/AccountType.java
index 21e17bd..15158dc 100644
--- a/src/com/android/contacts/model/AccountType.java
+++ b/src/com/android/contacts/model/AccountType.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.model;
 
+import com.android.contacts.R;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 import com.google.common.annotations.VisibleForTesting;
@@ -148,7 +149,14 @@
     /**
      * @return resource ID for the "invite contact" action label, or -1 if not defined.
      */
-    protected int getInviteContactActionResId(Context context) {
+    protected int getInviteContactActionResId() {
+        return -1;
+    }
+
+    /**
+     * @return resource ID for the "view group" label, or -1 if not defined.
+     */
+    protected int getViewGroupLabelResId() {
         return -1;
     }
 
@@ -174,8 +182,20 @@
      * the contact card.  (If not defined, returns null.)
      */
     public CharSequence getInviteContactActionLabel(Context context) {
-        return getResourceText(context, summaryResPackageName, getInviteContactActionResId(context),
-                "");
+        return getResourceText(context, summaryResPackageName, getInviteContactActionResId(), "");
+    }
+
+    /**
+     * Returns a label for the "view group" action. If not defined, this falls back to our
+     * own "View Updates" string
+     */
+    public CharSequence getViewGroupLabel(Context context) {
+        final CharSequence customTitle =
+                getResourceText(context, summaryResPackageName, getViewGroupLabelResId(), null);
+
+        return customTitle == null
+                ? context.getText(R.string.view_updates_from_group)
+                : customTitle;
     }
 
     /**
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index bdd8a50..5443196 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -85,7 +85,17 @@
         return new AccountTypeManagerImpl(context);
     }
 
-    public abstract List<AccountWithDataSet> getAccounts(boolean writableOnly);
+    /**
+     * Returns the list of all accounts (if contactWritableOnly is false) or just the list of
+     * contact writable accounts (if contactWritableOnly is true).
+     */
+    // TODO: Consider splitting this into getContactWritableAccounts() and getAllAccounts()
+    public abstract List<AccountWithDataSet> getAccounts(boolean contactWritableOnly);
+
+    /**
+     * Returns the list of accounts that are group writable.
+     */
+    public abstract List<AccountWithDataSet> getGroupWritableAccounts();
 
     public abstract AccountType getAccountType(AccountTypeWithDataSet accountTypeWithDataSet);
 
@@ -112,6 +122,13 @@
         final AccountType type = getAccountType(accountType, dataSet);
         return type == null ? null : type.getKindForMimetype(mimeType);
     }
+
+    /*
+     * Returns all registered {@link AccountType}s, including extension ones.
+     *
+     * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+     */
+    public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
 }
 
 class AccountTypeManagerImpl extends AccountTypeManager
@@ -123,7 +140,8 @@
     private AccountType mFallbackAccountType;
 
     private List<AccountWithDataSet> mAccounts = Lists.newArrayList();
-    private List<AccountWithDataSet> mWritableAccounts = Lists.newArrayList();
+    private List<AccountWithDataSet> mContactWritableAccounts = Lists.newArrayList();
+    private List<AccountWithDataSet> mGroupWritableAccounts = Lists.newArrayList();
     private Map<AccountTypeWithDataSet, AccountType> mAccountTypesWithDataSets = Maps.newHashMap();
     private Map<AccountTypeWithDataSet, AccountType> mInvitableAccountTypes =
             Collections.unmodifiableMap(new HashMap<AccountTypeWithDataSet, AccountType>());
@@ -281,16 +299,18 @@
         final long startTimeWall = SystemClock.elapsedRealtime();
 
         // Account types, keyed off the account type and data set concatenation.
-        Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet = Maps.newHashMap();
+        final Map<AccountTypeWithDataSet, AccountType> accountTypesByTypeAndDataSet =
+                Maps.newHashMap();
 
         // The same AccountTypes, but keyed off {@link RawContacts#ACCOUNT_TYPE}.  Since there can
         // be multiple account types (with different data sets) for the same type of account, each
         // type string may have multiple AccountType entries.
-        Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
+        final Map<String, List<AccountType>> accountTypesByType = Maps.newHashMap();
 
-        List<AccountWithDataSet> allAccounts = Lists.newArrayList();
-        List<AccountWithDataSet> writableAccounts = Lists.newArrayList();
-        Set<String> extensionPackages = Sets.newHashSet();
+        final List<AccountWithDataSet> allAccounts = Lists.newArrayList();
+        final List<AccountWithDataSet> contactWritableAccounts = Lists.newArrayList();
+        final List<AccountWithDataSet> groupWritableAccounts = Lists.newArrayList();
+        final Set<String> extensionPackages = Sets.newHashSet();
 
         final AccountManager am = mAccountManager;
         final IContentService cs = ContentResolver.getContentService();
@@ -395,7 +415,10 @@
                                 account.name, account.type, accountType.dataSet);
                         allAccounts.add(accountWithDataSet);
                         if (accountType.areContactsWritable()) {
-                            writableAccounts.add(accountWithDataSet);
+                            contactWritableAccounts.add(accountWithDataSet);
+                        }
+                        if (accountType.isGroupMembershipEditable()) {
+                            groupWritableAccounts.add(accountWithDataSet);
                         }
                     }
                 }
@@ -403,14 +426,16 @@
         }
 
         Collections.sort(allAccounts, ACCOUNT_COMPARATOR);
-        Collections.sort(writableAccounts, ACCOUNT_COMPARATOR);
+        Collections.sort(contactWritableAccounts, ACCOUNT_COMPARATOR);
+        Collections.sort(groupWritableAccounts, ACCOUNT_COMPARATOR);
 
         timings.addSplit("Loaded accounts");
 
         synchronized (this) {
             mAccountTypesWithDataSets = accountTypesByTypeAndDataSet;
             mAccounts = allAccounts;
-            mWritableAccounts = writableAccounts;
+            mContactWritableAccounts = contactWritableAccounts;
+            mGroupWritableAccounts = groupWritableAccounts;
             mInvitableAccountTypes = findInvitableAccountTypes(
                     mContext, allAccounts, accountTypesByTypeAndDataSet);
         }
@@ -460,12 +485,20 @@
     }
 
     /**
-     * Return list of all known, writable {@link AccountWithDataSet}'s.
+     * Return list of all known, contact writable {@link AccountWithDataSet}'s.
      */
     @Override
-    public List<AccountWithDataSet> getAccounts(boolean writableOnly) {
+    public List<AccountWithDataSet> getAccounts(boolean contactWritableOnly) {
         ensureAccountsLoaded();
-        return writableOnly ? mWritableAccounts : mAccounts;
+        return contactWritableOnly ? mContactWritableAccounts : mAccounts;
+    }
+
+    /**
+     * Return the list of all known, group writable {@link AccountWithDataSet}'s.
+     */
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        ensureAccountsLoaded();
+        return mGroupWritableAccounts;
     }
 
     /**
@@ -539,4 +572,17 @@
         }
         return Collections.unmodifiableMap(result);
     }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean contactWritableOnly) {
+        final List<AccountType> accountTypes = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mAccountTypesWithDataSets.values()) {
+                if (!contactWritableOnly || type.areContactsWritable()) {
+                    accountTypes.add(type);
+                }
+            }
+        }
+        return accountTypes;
+    }
 }
diff --git a/src/com/android/contacts/model/AccountWithDataSet.java b/src/com/android/contacts/model/AccountWithDataSet.java
index 55af795..e379346 100644
--- a/src/com/android/contacts/model/AccountWithDataSet.java
+++ b/src/com/android/contacts/model/AccountWithDataSet.java
@@ -17,21 +17,34 @@
 package com.android.contacts.model;
 
 import com.android.internal.util.Objects;
+import com.google.common.collect.Lists;
 
 import android.accounts.Account;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Parcel;
+import android.os.Parcelable.Creator;
 import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.RawContacts;
 import android.text.TextUtils;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
 /**
  * Wrapper for an account that includes a data set (which may be null).
  */
 public class AccountWithDataSet extends Account {
+    private static final String STRINGIFY_SEPARATOR = "\u0001";
+    private static final String ARRAY_STRINGIFY_SEPARATOR = "\u0002";
+
+    private static final Pattern STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(STRINGIFY_SEPARATOR));
+    private static final Pattern ARRAY_STRINGIFY_SEPARATOR_PAT =
+            Pattern.compile(Pattern.quote(ARRAY_STRINGIFY_SEPARATOR));
 
     public final String dataSet;
     private final AccountTypeWithDataSet mAccountTypeWithDataSet;
@@ -47,12 +60,29 @@
         mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
-    public AccountWithDataSet(Parcel in, String dataSet) {
+    public AccountWithDataSet(Parcel in) {
         super(in);
-        this.dataSet = dataSet;
+        this.dataSet = in.readString();
         mAccountTypeWithDataSet = AccountTypeWithDataSet.get(type, dataSet);
     }
 
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags);
+        dest.writeString(dataSet);
+    }
+
+    // For Parcelable
+    public static final Creator<AccountWithDataSet> CREATOR = new Creator<AccountWithDataSet>() {
+        public AccountWithDataSet createFromParcel(Parcel source) {
+            return new AccountWithDataSet(source);
+        }
+
+        public AccountWithDataSet[] newArray(int size) {
+            return new AccountWithDataSet[size];
+        }
+    };
+
     public AccountTypeWithDataSet getAccountTypeWithDataSet() {
         return mAccountTypeWithDataSet;
     }
@@ -100,4 +130,67 @@
     public String toString() {
         return "AccountWithDataSet {name=" + name + ", type=" + type + ", dataSet=" + dataSet + "}";
     }
+
+    private static StringBuilder addStringified(StringBuilder sb, AccountWithDataSet account) {
+        sb.append(account.name);
+        sb.append(STRINGIFY_SEPARATOR);
+        sb.append(account.type);
+        sb.append(STRINGIFY_SEPARATOR);
+        if (!TextUtils.isEmpty(account.dataSet)) sb.append(account.dataSet);
+
+        return sb;
+    }
+
+    /**
+     * Pack the instance into a string.
+     */
+    public String stringify() {
+        return addStringified(new StringBuilder(), this).toString();
+    }
+
+    /**
+     * Unpack a string created by {@link #stringify}.
+     */
+    public static AccountWithDataSet unstringify(String s) {
+        final String[] array = STRINGIFY_SEPARATOR_PAT.split(s, 3);
+        if (array.length < 3) {
+            throw new IllegalArgumentException("Invalid string");
+        }
+        return new AccountWithDataSet(array[0], array[1],
+                TextUtils.isEmpty(array[2]) ? null : array[2]);
+    }
+
+    /**
+     * Pack a list of {@link AccountWithDataSet} into a string.
+     */
+    public static String stringifyList(List<AccountWithDataSet> accounts) {
+        final StringBuilder sb = new StringBuilder();
+
+        for (AccountWithDataSet account : accounts) {
+            if (sb.length() > 0) {
+                sb.append(ARRAY_STRINGIFY_SEPARATOR);
+            }
+            addStringified(sb, account);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Unpack a list of {@link AccountWithDataSet} into a string.
+     */
+    public static List<AccountWithDataSet> unstringifyList(String s) {
+        final ArrayList<AccountWithDataSet> ret = Lists.newArrayList();
+        if (TextUtils.isEmpty(s)) {
+            return ret;
+        }
+
+        final String[] array = ARRAY_STRINGIFY_SEPARATOR_PAT.split(s);
+
+        for (int i = 0; i < array.length; i++) {
+            ret.add(unstringify(array[i]));
+        }
+
+        return ret;
+    }
 }
diff --git a/src/com/android/contacts/model/ExternalAccountType.java b/src/com/android/contacts/model/ExternalAccountType.java
index ca064c7..0518ea5 100644
--- a/src/com/android/contacts/model/ExternalAccountType.java
+++ b/src/com/android/contacts/model/ExternalAccountType.java
@@ -57,6 +57,7 @@
     private static final String ATTR_INVITE_CONTACT_ACTION_LABEL = "inviteContactActionLabel";
     private static final String ATTR_VIEW_CONTACT_NOTIFY_SERVICE = "viewContactNotifyService";
     private static final String ATTR_VIEW_GROUP_ACTIVITY = "viewGroupActivity";
+    private static final String ATTR_VIEW_GROUP_ACTION_LABEL = "viewGroupActionLabel";
     private static final String ATTR_VIEW_STREAM_ITEM_ACTIVITY = "viewStreamItemActivity";
     private static final String ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY =
             "viewStreamItemPhotoActivity";
@@ -75,12 +76,14 @@
     private String mCreateContactActivityClassName;
     private String mInviteContactActivity;
     private String mInviteActionLabelAttribute;
+    private int mInviteActionLabelResId;
     private String mViewContactNotifyService;
     private String mViewGroupActivity;
+    private String mViewGroupLabelAttribute;
+    private int mViewGroupLabelResId;
     private String mViewStreamItemActivity;
     private String mViewStreamItemPhotoActivity;
     private List<String> mExtensionPackageNames;
-    private int mInviteActionLabelResId;
     private String mAccountTypeLabelAttribute;
     private String mAccountTypeIconAttribute;
     private boolean mInitSuccessful;
@@ -111,6 +114,8 @@
         mExtensionPackageNames = new ArrayList<String>();
         mInviteActionLabelResId = resolveExternalResId(context, mInviteActionLabelAttribute,
                 summaryResPackageName, ATTR_INVITE_CONTACT_ACTION_LABEL);
+        mViewGroupLabelResId = resolveExternalResId(context, mViewGroupLabelAttribute,
+                summaryResPackageName, ATTR_VIEW_GROUP_ACTION_LABEL);
         titleRes = resolveExternalResId(context, mAccountTypeLabelAttribute,
                 this.resPackageName, ATTR_ACCOUNT_LABEL);
         iconRes = resolveExternalResId(context, mAccountTypeIconAttribute,
@@ -167,7 +172,7 @@
     }
 
     @Override
-    protected int getInviteContactActionResId(Context context) {
+    protected int getInviteContactActionResId() {
         return mInviteActionLabelResId;
     }
 
@@ -182,6 +187,11 @@
     }
 
     @Override
+    protected int getViewGroupLabelResId() {
+        return mViewGroupLabelResId;
+    }
+
+    @Override
     public String getViewStreamItemActivity() {
         return mViewStreamItemActivity;
     }
@@ -242,6 +252,8 @@
                     mViewContactNotifyService = value;
                 } else if (ATTR_VIEW_GROUP_ACTIVITY.equals(attr)) {
                     mViewGroupActivity = value;
+                } else if (ATTR_VIEW_GROUP_ACTION_LABEL.equals(attr)) {
+                    mViewGroupLabelAttribute = value;
                 } else if (ATTR_VIEW_STREAM_ITEM_ACTIVITY.equals(attr)) {
                     mViewStreamItemActivity = value;
                 } else if (ATTR_VIEW_STREAM_ITEM_PHOTO_ACTIVITY.equals(attr)) {
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index d065255..058cf84 100644
--- a/src/com/android/contacts/util/AccountsListAdapter.java
+++ b/src/com/android/contacts/util/AccountsListAdapter.java
@@ -42,20 +42,28 @@
     private final AccountTypeManager mAccountTypes;
     private final Context mContext;
 
-    public AccountsListAdapter(Context context, boolean writableOnly) {
-        this(context, writableOnly, null);
+    /**
+     * Filters that affect the list of accounts that is displayed by this adapter.
+     */
+    public enum AccountListFilter {
+        ALL_ACCOUNTS,                   // All read-only and writable accounts
+        ACCOUNTS_CONTACT_WRITABLE,      // Only where the account type is contact writable
+        ACCOUNTS_GROUP_WRITABLE         // Only accounts where the account type is group writable
+    }
+
+    public AccountsListAdapter(Context context, AccountListFilter accountListFilter) {
+        this(context, accountListFilter, null);
     }
 
     /**
      * @param currentAccount the Account currently selected by the user, which should come
      * first in the list. Can be null.
      */
-    public AccountsListAdapter(Context context, boolean writableOnly,
+    public AccountsListAdapter(Context context, AccountListFilter accountListFilter,
             AccountWithDataSet currentAccount) {
         mContext = context;
         mAccountTypes = AccountTypeManager.getInstance(context);
-        // We don't want possible side-effect toward AccountTypeManager
-        mAccounts = new ArrayList<AccountWithDataSet>(mAccountTypes.getAccounts(writableOnly));
+        mAccounts = getAccounts(accountListFilter);
         if (currentAccount != null
                 && !mAccounts.isEmpty()
                 && !mAccounts.get(0).equals(currentAccount)
@@ -65,24 +73,33 @@
         mInflater = LayoutInflater.from(context);
     }
 
+    private List<AccountWithDataSet> getAccounts(AccountListFilter accountListFilter) {
+        if (accountListFilter == AccountListFilter.ACCOUNTS_GROUP_WRITABLE) {
+            return new ArrayList<AccountWithDataSet>(mAccountTypes.getGroupWritableAccounts());
+        }
+        return new ArrayList<AccountWithDataSet>(mAccountTypes.getAccounts(
+                accountListFilter == AccountListFilter.ACCOUNTS_CONTACT_WRITABLE));
+    }
+
     @Override
     public View getView(int position, View convertView, ViewGroup parent) {
         final View resultView = convertView != null ? convertView
                 : mInflater.inflate(R.layout.account_selector_list_item, parent, false);
 
-        final TextView text1 = (TextView)resultView.findViewById(android.R.id.text1);
-        final TextView text2 = (TextView)resultView.findViewById(android.R.id.text2);
-        final ImageView icon = (ImageView)resultView.findViewById(android.R.id.icon);
+        final TextView text1 = (TextView) resultView.findViewById(android.R.id.text1);
+        final TextView text2 = (TextView) resultView.findViewById(android.R.id.text2);
+        final ImageView icon = (ImageView) resultView.findViewById(android.R.id.icon);
 
         final AccountWithDataSet account = mAccounts.get(position);
         final AccountType accountType = mAccountTypes.getAccountType(account.type, account.dataSet);
 
-        text1.setText(account.name);
+        text1.setText(accountType.getDisplayLabel(mContext));
 
         // For email addresses, we don't want to truncate at end, which might cut off the domain
         // name.
-        text1.setEllipsize(TruncateAt.MIDDLE);
-        text2.setText(accountType.getDisplayLabel(mContext));
+        text2.setText(account.name);
+        text2.setEllipsize(TruncateAt.MIDDLE);
+
         icon.setImageDrawable(accountType.getDisplayIcon(mContext));
 
         return resultView;
diff --git a/src/com/android/contacts/util/HtmlUtils.java b/src/com/android/contacts/util/HtmlUtils.java
index faaa9c2..c89e8c2 100644
--- a/src/com/android/contacts/util/HtmlUtils.java
+++ b/src/com/android/contacts/util/HtmlUtils.java
@@ -1,5 +1,8 @@
 package com.android.contacts.util;
 
+import com.android.contacts.R;
+import com.google.common.annotations.VisibleForTesting;
+
 import android.content.Context;
 import android.content.res.Resources;
 import android.text.Html;
@@ -11,8 +14,6 @@
 import android.text.style.ImageSpan;
 import android.text.style.QuoteSpan;
 
-import com.android.contacts.R;
-
 /**
  * Provides static functions to perform custom HTML to text conversions.
  * Specifically, it adjusts the color and padding of the vertical
@@ -21,43 +22,53 @@
 public class HtmlUtils {
 
     /**
-     * Converts HTML string to a {@link Spanned} text, adjusting formatting.
+     * Converts HTML string to a {@link Spanned} text, adjusting formatting. Any extra new line
+     * characters at the end of the text will be trimmed.
      */
     public static Spanned fromHtml(Context context, String text) {
         if (TextUtils.isEmpty(text)) {
             return null;
         }
         Spanned spanned = Html.fromHtml(text);
-        postprocess(context, spanned);
-        return spanned;
+        return postprocess(context, spanned);
     }
 
     /**
      * Converts HTML string to a {@link Spanned} text, adjusting formatting and using a custom
-     * image getter.
+     * image getter. Any extra new line characters at the end of the text will be trimmed.
      */
     public static CharSequence fromHtml(Context context, String text, ImageGetter imageGetter,
             TagHandler tagHandler) {
         if (TextUtils.isEmpty(text)) {
             return null;
         }
-        Spanned spanned = Html.fromHtml(text, imageGetter, tagHandler);
-        postprocess(context, spanned);
-        return spanned;
+        return postprocess(context, Html.fromHtml(text, imageGetter, tagHandler));
     }
 
     /**
-     * Replaces some spans with custom versions of those.
+     * Replaces some spans with custom versions of those. Any extra new line characters at the end
+     * of the text will be trimmed.
      */
-    private static void postprocess(Context context, Spanned spanned) {
-        if (!(spanned instanceof SpannableStringBuilder)) {
-            return;
+    @VisibleForTesting
+    static Spanned postprocess(Context context, Spanned original) {
+        if (original == null) {
+            return null;
+        }
+        final int length = original.length();
+        if (length == 0) {
+            return original; // Bail early.
         }
 
-        int length = spanned.length();
+        // If it's a SpannableStringBuilder, just use it.  Otherwise, create a new
+        // SpannableStringBuilder based on the passed Spanned.
+        final SpannableStringBuilder builder;
+        if (original instanceof SpannableStringBuilder) {
+            builder = (SpannableStringBuilder) original;
+        } else {
+            builder = new SpannableStringBuilder(original);
+        }
 
-        SpannableStringBuilder builder = (SpannableStringBuilder)spanned;
-        QuoteSpan[] quoteSpans = spanned.getSpans(0, length, QuoteSpan.class);
+        final QuoteSpan[] quoteSpans = builder.getSpans(0, length, QuoteSpan.class);
         if (quoteSpans != null && quoteSpans.length != 0) {
             Resources resources = context.getResources();
             int color = resources.getColor(R.color.stream_item_stripe_color);
@@ -67,7 +78,7 @@
             }
         }
 
-        ImageSpan[] imageSpans = spanned.getSpans(0, length, ImageSpan.class);
+        final ImageSpan[] imageSpans = builder.getSpans(0, length, ImageSpan.class);
         if (imageSpans != null) {
             for (int i = 0; i < imageSpans.length; i++) {
                 ImageSpan span = imageSpans[i];
@@ -75,6 +86,25 @@
                         ImageSpan.ALIGN_BASELINE));
             }
         }
+
+        // Trim the trailing new line characters at the end of the text (which can be added
+        // when HTML block quote tags are turned into new line characters).
+        int end = length;
+        for (int i = builder.length() - 1; i >= 0; i--) {
+            if (builder.charAt(i) != '\n') {
+                break;
+            }
+            end = i;
+        }
+
+        // If there's no trailing newlines, just return it.
+        if (end == length) {
+            return builder;
+        }
+
+        // Otherwise, Return a substring of the original {@link Spanned} text
+        // from the start index (inclusive) to the end index (exclusive).
+        return new SpannableStringBuilder(builder, 0, end);
     }
 
     /**
diff --git a/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
new file mode 100644
index 0000000..9f4e487
--- /dev/null
+++ b/tests/src/com/android/contacts/editor/ContactEditorUtilsTest.java
@@ -0,0 +1,310 @@
+/*
+ * 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.editor;
+
+import com.android.contacts.editor.ContactEditorUtils;
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.tests.mocks.MockAccountTypeManager;
+import com.google.android.collect.Sets;
+
+import android.content.Context;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Test case for {@link ContactEditorUtils}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.editor.ContactEditorUtilsTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class ContactEditorUtilsTest extends AndroidTestCase {
+    private MockAccountTypeManager mAccountTypes;
+    private ContactEditorUtils mTarget;
+
+    private static final MockAccountType TYPE1 = new MockAccountType("type1", null, true);
+    private static final MockAccountType TYPE2 = new MockAccountType("type2", null, true);
+    private static final MockAccountType TYPE2EX = new MockAccountType("type2", "ext", true);
+
+    // Only type 3 is "readonly".
+    private static final MockAccountType TYPE3 = new MockAccountType("type3", null, false);
+
+    private static final AccountWithDataSet ACCOUNT_1_A = new AccountWithDataSet(
+            "a", TYPE1.accountType, TYPE1.dataSet);
+    private static final AccountWithDataSet ACCOUNT_1_B = new AccountWithDataSet(
+            "b", TYPE1.accountType, TYPE1.dataSet);
+
+    private static final AccountWithDataSet ACCOUNT_2_A = new AccountWithDataSet(
+            "a", TYPE2.accountType, TYPE2.dataSet);
+    private static final AccountWithDataSet ACCOUNT_2EX_A = new AccountWithDataSet(
+            "a", TYPE2EX.accountType, TYPE2EX.dataSet);
+
+    private static final AccountWithDataSet ACCOUNT_3_C = new AccountWithDataSet(
+            "c", TYPE3.accountType, TYPE3.dataSet);
+
+    @Override
+    protected void setUp() throws Exception {
+        // Initialize with 0 types, 0 accounts.
+        mAccountTypes = new MockAccountTypeManager(new AccountType[] {},
+                new AccountWithDataSet[] {});
+        mTarget = new ContactEditorUtils(getContext(), mAccountTypes);
+
+        // Clear the preferences.
+        mTarget.cleanupForTest();
+    }
+
+    private void setAccountTypes(AccountType... types) {
+        mAccountTypes.mTypes = types;
+    }
+
+    private void setAccounts(AccountWithDataSet... accounts) {
+        mAccountTypes.mAccounts = accounts;
+    }
+
+    public void testGetWritableAccountTypeStrings() {
+        String[] types;
+
+        // 0 writable types
+        setAccountTypes();
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(types, new String[0]);
+
+        // 1 writable type
+        setAccountTypes(TYPE1);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(Sets.newHashSet(TYPE1.accountType), Sets.newHashSet(types));
+
+        // 2 writable types
+        setAccountTypes(TYPE1, TYPE2EX);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(Sets.newHashSet(TYPE1.accountType, TYPE2EX.accountType),
+                Sets.newHashSet(types));
+
+        // 3 writable types + 1 readonly type
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX, TYPE3);
+
+        types = mTarget.getWritableAccountTypeStrings();
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(TYPE1.accountType, TYPE2.accountType, TYPE2EX.accountType),
+                Sets.newHashSet(types));
+    }
+
+    /**
+     * Test for
+     * - {@link ContactEditorUtils#saveDefaultAndAllAccounts}
+     * - {@link ContactEditorUtils#getDefaultAccount}
+     * - {@link ContactEditorUtils#getSavedAccounts()}
+     */
+    public void testSaveDefaultAndAllAccounts() {
+        // Use these account types here.
+        setAccountTypes(TYPE1, TYPE2);
+
+        // If none has been saved, it should return an empty list.
+        assertEquals(0, mTarget.getSavedAccounts().size());
+
+        // Save 0 accounts.
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{};
+        mTarget.saveDefaultAndAllAccounts(null);
+        assertNull(mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+
+
+        // 1 account
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A};
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+        assertEquals(ACCOUNT_1_A, mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+
+        // 2 account
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A, ACCOUNT_1_B};
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_B);
+        assertEquals(ACCOUNT_1_B, mTarget.getDefaultAccount());
+        MoreAsserts.assertEquals(
+                Sets.newHashSet(mAccountTypes.mAccounts),
+                toSet(mTarget.getSavedAccounts()));
+    }
+
+    public void testIsAccountValid() {
+        // Use these account types here.
+        setAccountTypes(TYPE1, TYPE2);
+
+        // 0 accounts
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{};
+        assertFalse(mTarget.isValidAccount(ACCOUNT_1_A));
+        assertTrue(mTarget.isValidAccount(null)); // null is always valid
+
+        // 2 accounts
+        mAccountTypes.mAccounts = new AccountWithDataSet[]{ACCOUNT_1_A, ACCOUNT_2_A};
+        assertTrue(mTarget.isValidAccount(ACCOUNT_1_A));
+        assertTrue(mTarget.isValidAccount(ACCOUNT_2_A));
+        assertFalse(mTarget.isValidAccount(ACCOUNT_2EX_A));
+        assertTrue(mTarget.isValidAccount(null)); // null is always valid
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 0 accounts.
+     */
+    public void testShouldShowAccountChangedNotification_0Accounts() {
+        // There's always at least one writable type...
+        setAccountTypes(TYPE1);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // We show the notification here, and user clicked "add account"
+        setAccounts(ACCOUNT_1_A);
+
+        // Now we open the contact editor with the new account.
+
+        // When closing the editor, we save the default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+
+        // Next time the user creates a contact, we don't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // User added a new writable account, ACCOUNT_1_B.
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B);
+
+        // Now we show the notification again.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saved a new contact.  We update the account list and the default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_B);
+
+        // User created another contact.  Now we don't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // User installed a new contact sync adapter...
+
+        // Added a new account type: TYPE2, and the TYPE2EX extension.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        // Add new accounts: ACCOUNT_2_A, ACCOUNT_2EX_A.
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B, ACCOUNT_2_A, ACCOUNT_2EX_A);
+
+        // New account means another notification.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saves a new contact, with a different default account.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_2_A);
+
+        // Next time user creates a contact, no notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // Remove ACCOUNT_2EX_A.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B, ACCOUNT_2_A);
+
+        // ACCOUNT_2EX_A was not default, so no notification either.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // Remove ACCOUNT_1_B, which is default.
+        setAccountTypes(TYPE1, TYPE2, TYPE2EX);
+        setAccounts(ACCOUNT_1_A, ACCOUNT_1_B);
+
+        // Now we show the notification.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 1 accounts.
+     */
+    public void testShouldShowAccountChangedNotification_1Account() {
+        setAccountTypes(TYPE1, TYPE2);
+        setAccounts(ACCOUNT_1_A);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // User saves a new contact.
+        mTarget.saveDefaultAndAllAccounts(ACCOUNT_1_A);
+
+        // Next time, no notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+
+        // The rest is the same...
+    }
+
+    /**
+     * Tests for {@link ContactEditorUtils#shouldShowAccountChangedNotification()}, starting with
+     * 0 accounts, and the user selected "local only".
+     */
+    public void testShouldShowAccountChangedNotification_0Account_localOnly() {
+        // There's always at least one writable type...
+        setAccountTypes(TYPE1);
+
+        // First launch -- always true.
+        assertTrue(mTarget.shouldShowAccountChangedNotification());
+
+        // We show the notification here, and user clicked "keep local" and saved an contact.
+        mTarget.saveDefaultAndAllAccounts(null);
+
+        // Now there are no accounts, and default account is null.
+
+        // The user created another contact, but this we shouldn't show the notification.
+        assertFalse(mTarget.shouldShowAccountChangedNotification());
+    }
+
+    private static <T> Set<T> toSet(Collection<T> collection) {
+        Set<T> ret = Sets.newHashSet();
+        ret.addAll(collection);
+        return ret;
+    }
+
+    private static class MockAccountType extends AccountType {
+        private boolean mAreContactsWritable;
+
+        public MockAccountType(String accountType, String dataSet, boolean areContactsWritable) {
+            this.accountType = accountType;
+            this.dataSet = dataSet;
+            mAreContactsWritable = areContactsWritable;
+        }
+
+        @Override
+        public boolean areContactsWritable() {
+            return mAreContactsWritable;
+        }
+
+        @Override
+        public int getHeaderColor(Context context) {
+            return 0;
+        }
+
+        @Override
+        public int getSideBarColor(Context context) {
+            return 0;
+        }
+
+        @Override
+        public boolean isGroupMembershipEditable() {
+            return true;
+        }
+    }
+}
diff --git a/tests/src/com/android/contacts/model/AccountTypeTest.java b/tests/src/com/android/contacts/model/AccountTypeTest.java
index 9f7e7a2..42fe200 100644
--- a/tests/src/com/android/contacts/model/AccountTypeTest.java
+++ b/tests/src/com/android/contacts/model/AccountTypeTest.java
@@ -69,7 +69,7 @@
                 resPackageName = packageName;
                 summaryResPackageName = packageName;
             }
-            @Override protected int getInviteContactActionResId(Context conext) {
+            @Override protected int getInviteContactActionResId() {
                 return externalResID;
             }
 
diff --git a/tests/src/com/android/contacts/model/AccountWithDataSetTest.java b/tests/src/com/android/contacts/model/AccountWithDataSetTest.java
new file mode 100644
index 0000000..27c106e
--- /dev/null
+++ b/tests/src/com/android/contacts/model/AccountWithDataSetTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.model;
+
+import com.google.common.collect.Lists;
+
+import android.os.Bundle;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.List;
+
+/**
+ * Test case for {@link AccountWithDataSet}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.model.AccountWithDataSetTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class AccountWithDataSetTest extends AndroidTestCase {
+    public void testStringifyAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // stringify() & unstringify
+        AccountWithDataSet a1r = AccountWithDataSet.unstringify(a1.stringify());
+        AccountWithDataSet a2r = AccountWithDataSet.unstringify(a2.stringify());
+        AccountWithDataSet a3r = AccountWithDataSet.unstringify(a3.stringify());
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+
+    public void testStringifyListAndUnstringify() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Empty list
+        assertEquals(0, stringifyListAndUnstringify().size());
+
+        // 1 element
+        final List<AccountWithDataSet> listA = stringifyListAndUnstringify(a1);
+        assertEquals(1, listA.size());
+        assertEquals(a1, listA.get(0));
+
+        // 2 elements
+        final List<AccountWithDataSet> listB = stringifyListAndUnstringify(a2, a1);
+        assertEquals(2, listB.size());
+        assertEquals(a2, listB.get(0));
+        assertEquals(a1, listB.get(1));
+
+        // 3 elements
+        final List<AccountWithDataSet> listC = stringifyListAndUnstringify(a3, a2, a1);
+        assertEquals(3, listC.size());
+        assertEquals(a3, listC.get(0));
+        assertEquals(a2, listC.get(1));
+        assertEquals(a1, listC.get(2));
+    }
+
+    private static List<AccountWithDataSet> stringifyListAndUnstringify(
+            AccountWithDataSet... accounts) {
+
+        List<AccountWithDataSet> list = Lists.newArrayList(accounts);
+        return AccountWithDataSet.unstringifyList(AccountWithDataSet.stringifyList(list));
+    }
+
+    public void testParcelable() {
+        AccountWithDataSet a1 = new AccountWithDataSet("name1", "typeA", null);
+        AccountWithDataSet a2 = new AccountWithDataSet("name2", "typeB", null);
+        AccountWithDataSet a3 = new AccountWithDataSet("name3", "typeB", "dataset");
+
+        // Parcel them & unpercel.
+        final Bundle b = new Bundle();
+        b.putParcelable("a1", a1);
+        b.putParcelable("a2", a2);
+        b.putParcelable("a3", a3);
+
+        AccountWithDataSet a1r = b.getParcelable("a1");
+        AccountWithDataSet a2r = b.getParcelable("a2");
+        AccountWithDataSet a3r = b.getParcelable("a3");
+
+        assertEquals(a1, a1r);
+        assertEquals(a2, a2r);
+        assertEquals(a3, a3r);
+
+        MoreAsserts.assertNotEqual(a1, a2r);
+        MoreAsserts.assertNotEqual(a1, a3r);
+
+        MoreAsserts.assertNotEqual(a2, a1r);
+        MoreAsserts.assertNotEqual(a2, a3r);
+
+        MoreAsserts.assertNotEqual(a3, a1r);
+        MoreAsserts.assertNotEqual(a3, a2r);
+    }
+}
diff --git a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
index 7a04ae3..5ca1ccd 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
@@ -19,9 +19,11 @@
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountTypeWithDataSet;
 import com.android.contacts.model.AccountWithDataSet;
+import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
@@ -32,8 +34,8 @@
  */
 public class MockAccountTypeManager extends AccountTypeManager {
 
-    private final AccountType[] mTypes;
-    private AccountWithDataSet[] mAccounts;
+    public AccountType[] mTypes;
+    public AccountWithDataSet[] mAccounts;
 
     public MockAccountTypeManager(AccountType[] types, AccountWithDataSet[] accounts) {
         this.mTypes = types;
@@ -57,7 +59,25 @@
     }
 
     @Override
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
     public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
         return Maps.newHashMap(); // Always returns empty
     }
+
+    @Override
+    public List<AccountType> getAccountTypes(boolean writableOnly) {
+        final List<AccountType> ret = Lists.newArrayList();
+        synchronized (this) {
+            for (AccountType type : mTypes) {
+                if (!writableOnly || type.areContactsWritable()) {
+                    ret.add(type);
+                }
+            }
+        }
+        return ret;
+    }
 }
diff --git a/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java b/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
index a98b365..51c665f 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockContactPhotoManager.java
@@ -27,13 +27,15 @@
  */
 public class MockContactPhotoManager extends ContactPhotoManager {
     @Override
-    public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme) {
-        view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+    public void loadPhoto(ImageView view, long photoId, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, hires, darkTheme);
     }
 
     @Override
-    public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme) {
-        view.setImageResource(getDefaultAvatarResId(hires, darkTheme));
+    public void loadPhoto(ImageView view, Uri photoUri, boolean hires, boolean darkTheme,
+            DefaultImageProvider defaultProvider) {
+        defaultProvider.applyDefaultImage(view, hires, darkTheme);
     }
 
     @Override
diff --git a/tests/src/com/android/contacts/util/HtmlUtilsTest.java b/tests/src/com/android/contacts/util/HtmlUtilsTest.java
new file mode 100644
index 0000000..115f289
--- /dev/null
+++ b/tests/src/com/android/contacts/util/HtmlUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.util;
+
+import com.android.contacts.util.HtmlUtils.StreamItemQuoteSpan;
+
+import android.graphics.drawable.ColorDrawable;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.style.ImageSpan;
+import android.text.style.QuoteSpan;
+
+/**
+ * Tests for {@link HtmlUtils}.
+ *
+ * adb shell am instrument -w -e class com.android.contacts.util.HtmlUtilsTest \
+       com.android.contacts.tests/android.test.InstrumentationTestRunner
+ */
+@SmallTest
+public class HtmlUtilsTest extends AndroidTestCase {
+    /**
+     * Test for {@link HtmlUtils#postprocess} specifically about trimming newlines.
+     */
+    public void testPostProcess_trimNewLines() {
+        checkTrimNewLines("", "");
+        checkTrimNewLines("", "\n");
+        checkTrimNewLines("", "\n\n");
+        checkTrimNewLines("a", "a");
+        checkTrimNewLines("abc", "abc");
+        checkTrimNewLines("abc", "abc\n");
+        checkTrimNewLines("abc", "abc\n\n\n");
+        checkTrimNewLines("ab\nc", "ab\nc\n");
+
+        assertNull(HtmlUtils.postprocess(getContext(), null));
+    }
+
+    private final void checkTrimNewLines(String expectedString, CharSequence text) {
+        // Test with both SpannedString and SpannableStringBuilder.
+        assertEquals(expectedString,
+                HtmlUtils.postprocess(getContext(), new SpannedString(text)).toString());
+
+        assertEquals(expectedString,
+                HtmlUtils.postprocess(getContext(), new SpannableStringBuilder(text)).toString());
+    }
+
+    public void testPostProcess_with_newlines() {
+        final SpannableStringBuilder builder = new SpannableStringBuilder("01234\n\n");
+
+        setSpans(builder);
+
+        // First test with a SpannableStringBuilder, as opposed to SpannedString
+        checkPostProcess(HtmlUtils.postprocess(getContext(), builder));
+
+        // Then pass a SpannedString, which is immutable, but the method should still work.
+        checkPostProcess(HtmlUtils.postprocess(getContext(), new SpannedString(builder)));
+    }
+
+    /**
+     * Same as {@link #testPostProcess_with_newlines}, but text has no newlines.
+     * (The internal code path is slightly different.)
+     */
+    public void testPostProcess_no_newlines() {
+        final SpannableStringBuilder builder = new SpannableStringBuilder("01234");
+
+        setSpans(builder);
+
+        // First test with a SpannableStringBuilder, as opposed to SpannedString
+        checkPostProcess(HtmlUtils.postprocess(getContext(), builder));
+
+        // Then pass a SpannedString, which is immutable, but the method should still work.
+        checkPostProcess(HtmlUtils.postprocess(getContext(), new SpannedString(builder)));
+    }
+
+    private void setSpans(SpannableStringBuilder builder) {
+        builder.setSpan(new ImageSpan(new ColorDrawable(), ImageSpan.ALIGN_BOTTOM), 0, 2, 0);
+        builder.setSpan(new QuoteSpan(), 2, 4, 0);
+        builder.setSpan(new CustomSpan(), 4, builder.length(), 0);
+    }
+
+    private void checkPostProcess(Spanned ret) {
+        // Newlines should be trimmed.
+        assertEquals("01234", ret.toString());
+
+        // First, check the image span.
+        // - Vertical alignment should be changed to ALIGN_BASELINE
+        // - Drawable shouldn't be changed.
+        ImageSpan[] imageSpans = ret.getSpans(0, ret.length(), ImageSpan.class);
+        assertEquals(1, imageSpans.length);
+        assertEquals(ImageSpan.ALIGN_BASELINE, imageSpans[0].getVerticalAlignment());
+        assertEquals(ColorDrawable.class, imageSpans[0].getDrawable().getClass());
+
+        // QuoteSpans should be replaced with StreamItemQuoteSpans.
+        QuoteSpan[] quoteSpans = ret.getSpans(0, ret.length(), QuoteSpan.class);
+        assertEquals(1, quoteSpans.length);
+        assertEquals(StreamItemQuoteSpan.class, quoteSpans[0].getClass());
+
+        // Other spans should be preserved.
+        CustomSpan[] customSpans = ret.getSpans(0, ret.length(), CustomSpan.class);
+        assertEquals(1, customSpans.length);
+    }
+
+    /** Custom span class used in {@link #testPostProcess} */
+    private static class CustomSpan {
+    }
+}