Merge "Improve accessibility of call log's call button."
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/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/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/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_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/strings.xml b/res/values/strings.xml
index fde7b87..f4b94af 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1579,7 +1579,7 @@
     <!-- Label to instruct the user to type in a contact's name to add the contact as a member of the current group. [CHAR LIMIT=64] -->
     <string name="enter_contact_name">Type person\'s name</string>
 
-    <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=20] -->
+    <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=25] -->
     <string name="view_updates_from_group">View updates</string>
 
     <!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 2697589..be84cc4 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -79,6 +79,7 @@
     public static final String EXTRA_CONTACT_STATE = "state";
     public static final String EXTRA_SAVE_MODE = "saveMode";
     public static final String EXTRA_SAVE_IS_PROFILE = "saveIsProfile";
+    public static final String EXTRA_SAVE_SUCCEEDED = "saveSucceeded";
 
     public static final String ACTION_CREATE_GROUP = "createGroup";
     public static final String ACTION_RENAME_GROUP = "renameGroup";
@@ -345,6 +346,10 @@
                     lookupUri = RawContacts.getContactLookupUri(resolver, rawContactUri);
                 }
                 Log.v(TAG, "Saved contact. New URI: " + lookupUri);
+                // Mark the intent to indicate that the save was successful (even if the lookup URI
+                // is now null).  For local contacts or the local profile, it's possible that the
+                // save triggered removal of the contact, so no lookup URI would exist..
+                callbackIntent.putExtra(EXTRA_SAVE_SUCCEEDED, true);
                 break;
 
             } catch (RemoteException e) {
diff --git a/src/com/android/contacts/activities/ContactEditorActivity.java b/src/com/android/contacts/activities/ContactEditorActivity.java
index abdde87..07f340e 100644
--- a/src/com/android/contacts/activities/ContactEditorActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorActivity.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.activities;
 
+import com.android.contacts.ContactSaveService;
 import com.android.contacts.ContactsActivity;
 import com.android.contacts.R;
 import com.android.contacts.editor.ContactEditorFragment;
@@ -115,6 +116,7 @@
         } else if (ACTION_SAVE_COMPLETED.equals(action)) {
             mFragment.onSaveCompleted(true,
                     intent.getIntExtra(ContactEditorFragment.SAVE_MODE_EXTRA_KEY, SaveMode.CLOSE),
+                    intent.getBooleanExtra(ContactSaveService.EXTRA_SAVE_SUCCEEDED, false),
                     intent.getData());
         } else if (ACTION_JOIN_COMPLETED.equals(action)) {
             mFragment.onJoinCompleted(intent.getData());
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/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index ce5bf8d..b81cebf 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -350,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 e9fbbbb..d1c5868 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -899,7 +899,12 @@
                 // Clear the digits just in case.
                 mDigits.getText().clear();
             } else {
-                startActivity(newDialNumberIntent(number));
+                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();
             }
diff --git a/src/com/android/contacts/editor/ContactEditorFragment.java b/src/com/android/contacts/editor/ContactEditorFragment.java
index b341f83..f7e0c23 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -398,7 +398,7 @@
         mAutoAddToDefaultGroup = mIntentExtras != null
                 && mIntentExtras.containsKey(INTENT_EXTRA_ADD_TO_DEFAULT_DIRECTORY);
         mNewLocalProfile = mIntentExtras != null
-            && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
+                && mIntentExtras.getBoolean(INTENT_EXTRA_NEW_LOCAL_PROFILE);
     }
 
     public void setListener(Listener value) {
@@ -1033,7 +1033,7 @@
 
         final AccountTypeManager accountTypes = AccountTypeManager.getInstance(mContext);
         if (!EntityModifier.hasChanges(mState, accountTypes)) {
-            onSaveCompleted(false, saveMode, mLookupUri);
+            onSaveCompleted(false, saveMode, mLookupUri != null, mLookupUri);
             return true;
         }
 
@@ -1099,14 +1099,14 @@
     }
 
     public void onJoinCompleted(Uri uri) {
-        onSaveCompleted(false, SaveMode.RELOAD, uri);
+        onSaveCompleted(false, SaveMode.RELOAD, uri != null, uri);
     }
 
-    public void onSaveCompleted(boolean hadChanges, int saveMode, Uri contactLookupUri) {
-        boolean success = contactLookupUri != null;
+    public void onSaveCompleted(boolean hadChanges, int saveMode, boolean saveSucceeded,
+            Uri contactLookupUri) {
         Log.d(TAG, "onSaveCompleted(" + saveMode + ", " + contactLookupUri);
         if (hadChanges) {
-            if (success) {
+            if (saveSucceeded) {
                 if (saveMode != SaveMode.JOIN) {
                     Toast.makeText(mContext, R.string.contactSavedToast, Toast.LENGTH_SHORT).show();
                 }
@@ -1118,7 +1118,7 @@
             case SaveMode.CLOSE:
             case SaveMode.HOME:
                 final Intent resultIntent;
-                if (success && contactLookupUri != null) {
+                if (saveSucceeded && contactLookupUri != null) {
                     final String requestAuthority =
                             mLookupUri == null ? null : mLookupUri.getAuthority();
 
@@ -1149,7 +1149,7 @@
 
             case SaveMode.RELOAD:
             case SaveMode.JOIN:
-                if (success && contactLookupUri != null) {
+                if (saveSucceeded && contactLookupUri != null) {
                     // If it was a JOIN, we are now ready to bring up the join activity.
                     if (saveMode == SaveMode.JOIN) {
                         showJoinAggregateActivity(contactLookupUri);
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/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/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 608d3ad..30c3c48 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -49,7 +49,7 @@
     private View mProfileHeader;
     private Button mProfileMessage;
     private FrameLayout mMessageContainer;
-    private View mProfileTitle;
+    private TextView mProfileTitle;
 
     private View mPaddingView;
 
@@ -273,7 +273,8 @@
         mProfileHeaderContainer = new FrameLayout(inflater.getContext());
         mProfileHeader = inflater.inflate(R.layout.user_profile_header, null, false);
         mCounterHeaderView = (TextView) mProfileHeader.findViewById(R.id.contacts_count);
-        mProfileTitle = mProfileHeader.findViewById(R.id.profile_title);
+        mProfileTitle = (TextView) mProfileHeader.findViewById(R.id.profile_title);
+        mProfileTitle.setAllCaps(true);
         mProfileHeaderContainer.addView(mProfileHeader);
         list.addHeaderView(mProfileHeaderContainer, null, false);
 
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/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/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/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/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 {
+    }
+}