Merge "Allow sync adapter to rename the "View Updates" 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/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/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/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
index d63ea6a..3e2a893 100644
--- a/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
+++ b/src/com/android/contacts/activities/ContactEditorAccountsChangedActivity.java
@@ -16,7 +16,6 @@
 
 package com.android.contacts.activities;
 
-import android.accounts.Account;
 import android.app.Activity;
 import android.content.Intent;
 import android.os.Bundle;
@@ -34,6 +33,7 @@
 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;
 
@@ -97,7 +97,8 @@
             button.setOnClickListener(mAddAccountClickListener);
 
             final ListView accountListView = (ListView) findViewById(R.id.account_list);
-            mAccountListAdapter = new AccountsListAdapter(this, true);
+            mAccountListAdapter = new AccountsListAdapter(this,
+                    AccountListFilter.ACCOUNTS_CONTACT_WRITABLE);
             accountListView.setAdapter(mAccountListAdapter);
             accountListView.setOnItemClickListener(mAccountListItemClickListener);
         } else if (numAccounts == 1) {
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 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 412ceff..d1c5868 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -44,6 +44,7 @@
 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;
@@ -882,9 +883,13 @@
         } else {
             final String number = mDigits.getText().toString();
 
+            // "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)) {
+                    && 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();
@@ -894,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 062c021..b341f83 100644
--- a/src/com/android/contacts/editor/ContactEditorFragment.java
+++ b/src/com/android/contacts/editor/ContactEditorFragment.java
@@ -34,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;
@@ -826,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);
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/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/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index dc2fb0d..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);
 
@@ -130,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>());
@@ -288,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();
@@ -402,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);
                         }
                     }
                 }
@@ -410,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);
         }
@@ -467,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;
     }
 
     /**
diff --git a/src/com/android/contacts/util/AccountsListAdapter.java b/src/com/android/contacts/util/AccountsListAdapter.java
index fc48e72..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,6 +73,14 @@
         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
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/tests/mocks/MockAccountTypeManager.java b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
index 3b712c7..5ca1ccd 100644
--- a/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
+++ b/tests/src/com/android/contacts/tests/mocks/MockAccountTypeManager.java
@@ -59,6 +59,11 @@
     }
 
     @Override
+    public List<AccountWithDataSet> getGroupWritableAccounts() {
+        return Arrays.asList(mAccounts);
+    }
+
+    @Override
     public Map<AccountTypeWithDataSet, AccountType> getInvitableAccountTypes() {
         return Maps.newHashMap(); // Always returns empty
     }
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 {
+    }
+}