Merge "Relax phone number collapser"
diff --git a/res/layout/stream_item_container.xml b/res/layout/stream_item_container.xml
index 6fa15b1..ee32596 100644
--- a/res/layout/stream_item_container.xml
+++ b/res/layout/stream_item_container.xml
@@ -50,20 +50,31 @@
             android:layout_height="wrap_content"
             android:textSize="16sp"
             android:textColor="?android:attr/textColorPrimary" />
-        <TextView android:id="@+id/stream_item_attribution"
-            android:layout_width="wrap_content"
+        <!--
+        Attribution (e.g. timestamp) and comments (e.g. +1, like) should align horizontally.
+        Can't merge this with the parent list view.
+        -->
+        <LinearLayout
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary"
-            android:ellipsize="end"
-            android:maxLines="1" />
-        <TextView android:id="@+id/stream_item_comments"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_marginLeft="@dimen/detail_update_section_attribution_comments_padding"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorSecondary"
-            android:maxLines="1"/>
+            android:orientation="horizontal"
+            >
+            <TextView android:id="@+id/stream_item_attribution"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="?android:attr/textColorSecondary"
+                android:ellipsize="end"
+                android:maxLines="1" />
+            <TextView android:id="@+id/stream_item_comments"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft=
+                    "@dimen/detail_update_section_attribution_comments_padding"
+                android:textAppearance="?android:attr/textAppearanceSmall"
+                android:textColor="?android:attr/textColorSecondary"
+                android:maxLines="1"/>
+        </LinearLayout>
     </LinearLayout>
 
     <View
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index 405ba6f..17cd1e7 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -25,6 +25,7 @@
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.android.contacts.util.UriUtils;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 
@@ -732,8 +733,7 @@
                 if (!resultIsCached) loadPhotoBinaryData(result);
 
                 // Note ME profile should never have "Add connection"
-                if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null &&
-                        !result.isUserProfile()) {
+                if (mLoadInvitableAccountTypes && result.getInvitableAccountTypes() == null) {
                     loadInvitableAccountTypes(result);
                 }
             }
@@ -855,25 +855,29 @@
      * Sets the "invitable" account types to {@link Result#mInvitableAccountTypes}.
      */
     private void loadInvitableAccountTypes(Result contactData) {
-        Map<AccountTypeWithDataSet, AccountType> invitables =
-                AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
-        if (invitables.isEmpty()) {
-            return;
-        }
+        final ArrayList<AccountType> resultList = Lists.newArrayList();
+        if (!contactData.isUserProfile()) {
+            Map<AccountTypeWithDataSet, AccountType> invitables =
+                    AccountTypeManager.getInstance(getContext()).getUsableInvitableAccountTypes();
+            if (!invitables.isEmpty()) {
+                final Map<AccountTypeWithDataSet, AccountType> resultMap =
+                        Maps.newHashMap(invitables);
 
-        Map<AccountTypeWithDataSet, AccountType> result = Maps.newHashMap(invitables);
+                // Remove the ones that already have a raw contact in the current contact
+                for (Entity entity : contactData.getEntities()) {
+                    final ContentValues values = entity.getEntityValues();
+                    final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
+                            values.getAsString(RawContacts.ACCOUNT_TYPE),
+                            values.getAsString(RawContacts.DATA_SET));
+                    resultMap.remove(type);
+                }
 
-        // Remove the ones that already have a raw contact in the current contact
-        for (Entity entity : contactData.getEntities()) {
-            final ContentValues values = entity.getEntityValues();
-            final AccountTypeWithDataSet type = AccountTypeWithDataSet.get(
-                    values.getAsString(RawContacts.ACCOUNT_TYPE),
-                    values.getAsString(RawContacts.DATA_SET));
-            result.remove(type);
+                resultList.addAll(resultMap.values());
+            }
         }
 
         // Set to mInvitableAccountTypes
-        contactData.mInvitableAccountTypes = new ArrayList<AccountType>(result.values());
+        contactData.mInvitableAccountTypes = resultList;
     }
 
     /**
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 4089434..ae8fe09 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -497,6 +497,7 @@
         mViewPager = (ViewPager) findViewById(R.id.pager);
         mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
         mViewPager.setOnPageChangeListener(mPageChangeListener);
+        mViewPager.setOffscreenPageLimit(2);
 
         // Do same width calculation as ActionBar does
         DisplayMetrics dm = getResources().getDisplayMetrics();
diff --git a/src/com/android/contacts/list/PhoneFavoriteFragment.java b/src/com/android/contacts/list/PhoneFavoriteFragment.java
index 2e62d1a..011a811 100644
--- a/src/com/android/contacts/list/PhoneFavoriteFragment.java
+++ b/src/com/android/contacts/list/PhoneFavoriteFragment.java
@@ -237,10 +237,57 @@
     private final ScrollListener mScrollListener = new ScrollListener();
 
     @Override
+    public void onAttach(Activity activity) {
+        if (DEBUG) Log.d(TAG, "onAttach()");
+        super.onAttach(activity);
+
+        mContactsPrefs = new ContactsPreferences(activity);
+
+        // Construct two base adapters which will become part of PhoneFavoriteMergedAdapter.
+        // We don't construct the resultant adapter at this moment since it requires LayoutInflater
+        // that will be available on onCreateView().
+
+        mContactTileAdapter = new ContactTileAdapter(activity, mContactTileAdapterListener,
+                getResources().getInteger(R.integer.contact_tile_column_count),
+                ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
+        mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
+
+        // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
+        mAllContactsAdapter = new PhoneNumberListAdapter(activity);
+        mAllContactsAdapter.setDisplayPhotos(true);
+        mAllContactsAdapter.setQuickContactEnabled(true);
+        mAllContactsAdapter.setSearchMode(false);
+        mAllContactsAdapter.setIncludeProfile(false);
+        mAllContactsAdapter.setSelectionVisible(false);
+        mAllContactsAdapter.setDarkTheme(true);
+        mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(activity));
+        // Disable directory header.
+        mAllContactsAdapter.setHasHeader(0, false);
+        // Show A-Z section index.
+        mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
+        // Disable pinned header. It doesn't work with this fragment.
+        mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
+        // Put photos on left for consistency with "frequent" contacts section.
+        mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
+
+        // Use Callable.CONTENT_URI which will include not only phone numbers but also SIP
+        // addresses.
+        mAllContactsAdapter.setUseCallableUri(true);
+
+        mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+        mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
+    }
+
+    @Override
     public void onCreate(Bundle savedState) {
+        if (DEBUG) Log.d(TAG, "onCreate()");
         super.onCreate(savedState);
         if (savedState != null) {
             mFilter = savedState.getParcelable(KEY_FILTER);
+
+            if (mFilter != null) {
+                mAllContactsAdapter.setFilter(mFilter);
+            }
         }
         setHasOptionsMenu(true);
     }
@@ -252,13 +299,6 @@
     }
 
     @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-
-        mContactsPrefs = new ContactsPreferences(activity);
-    }
-
-    @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
             Bundle savedInstanceState) {
         final View listLayout = inflater.inflate(
@@ -271,7 +311,16 @@
         mListView.setVerticalScrollbarPosition(View.SCROLLBAR_POSITION_RIGHT);
         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
 
-        initAdapters(getActivity(), inflater);
+        // Create the account filter header but keep it hidden until "all" contacts are loaded.
+        mAccountFilterHeaderContainer = new FrameLayout(getActivity(), null);
+        mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
+                mListView, false);
+        mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
+        mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
+        mAccountFilterHeaderContainer.setVisibility(View.GONE);
+
+        mAdapter = new PhoneFavoriteMergedAdapter(getActivity(),
+                mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter);
 
         mListView.setAdapter(mAdapter);
 
@@ -288,59 +337,6 @@
         return listLayout;
     }
 
-    /**
-     * Constructs and initializes {@link #mContactTileAdapter}, {@link #mAllContactsAdapter}, and
-     * {@link #mAllContactsAdapter}.
-     *
-     * TODO: Move all the code here to {@link PhoneFavoriteMergedAdapter} if possible.
-     * There are two problems: account header (whose content changes depending on filter settings)
-     * and OnClickListener (which initiates {@link Activity#startActivityForResult(Intent, int)}).
-     * See also issue 5429203, 5269692, and 5432286. If we are able to have a singleton for filter,
-     * this work will become easier.
-     */
-    private void initAdapters(Context context, LayoutInflater inflater) {
-        mContactTileAdapter = new ContactTileAdapter(context, mContactTileAdapterListener,
-                getResources().getInteger(R.integer.contact_tile_column_count),
-                ContactTileAdapter.DisplayType.STREQUENT_PHONE_ONLY);
-        mContactTileAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
-
-        // Setup the "all" adapter manually. See also the setup logic in ContactEntryListFragment.
-        mAllContactsAdapter = new PhoneNumberListAdapter(context);
-        mAllContactsAdapter.setDisplayPhotos(true);
-        mAllContactsAdapter.setQuickContactEnabled(true);
-        mAllContactsAdapter.setSearchMode(false);
-        mAllContactsAdapter.setIncludeProfile(false);
-        mAllContactsAdapter.setSelectionVisible(false);
-        mAllContactsAdapter.setDarkTheme(true);
-        mAllContactsAdapter.setPhotoLoader(ContactPhotoManager.getInstance(context));
-        // Disable directory header.
-        mAllContactsAdapter.setHasHeader(0, false);
-        // Show A-Z section index.
-        mAllContactsAdapter.setSectionHeaderDisplayEnabled(true);
-        // Disable pinned header. It doesn't work with this fragment.
-        mAllContactsAdapter.setPinnedPartitionHeadersEnabled(false);
-        // Put photos on left for consistency with "frequent" contacts section.
-        mAllContactsAdapter.setPhotoPosition(ContactListItemView.PhotoPosition.LEFT);
-
-        mAllContactsAdapter.setUseCallableUri(true);
-
-        if (mFilter != null) {
-            mAllContactsAdapter.setFilter(mFilter);
-        }
-
-        // Create the account filter header but keep it hidden until "all" contacts are loaded.
-        mAccountFilterHeaderContainer = new FrameLayout(context, null);
-        mAccountFilterHeader = inflater.inflate(R.layout.account_filter_header_for_phone_favorite,
-                mListView, false);
-        mAccountFilterHeader.setOnClickListener(mFilterHeaderClickListener);
-        mAccountFilterHeaderContainer.addView(mAccountFilterHeader);
-        mAccountFilterHeaderContainer.setVisibility(View.GONE);
-
-        mAdapter = new PhoneFavoriteMergedAdapter(context,
-                mContactTileAdapter, mAccountFilterHeaderContainer, mAllContactsAdapter);
-
-    }
-
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
         super.onCreateOptionsMenu(menu, inflater);
@@ -381,7 +377,7 @@
             mAllContactsForceReload = true;
         }
 
-        // Use initLoader() instead of reloadLoader() to refraing unnecessary reload.
+        // Use initLoader() instead of restartLoader() to refraining unnecessary reload.
         // This method call implicitly assures ContactTileLoaderListener's onLoadFinished() will
         // be called, on which we'll check if "all" contacts should be reloaded again or not.
         getLoaderManager().initLoader(LOADER_ID_CONTACT_TILE, null, mContactTileLoaderListener);
@@ -431,13 +427,15 @@
         }
 
         boolean changed = false;
-        if (mAllContactsAdapter.getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
-            mAllContactsAdapter.setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
+        final int currentDisplayOrder = mContactsPrefs.getDisplayOrder();
+        if (mAllContactsAdapter.getContactNameDisplayOrder() != currentDisplayOrder) {
+            mAllContactsAdapter.setContactNameDisplayOrder(currentDisplayOrder);
             changed = true;
         }
 
-        if (mAllContactsAdapter.getSortOrder() != mContactsPrefs.getSortOrder()) {
-            mAllContactsAdapter.setSortOrder(mContactsPrefs.getSortOrder());
+        final int currentSortOrder = mContactsPrefs.getSortOrder();
+        if (mAllContactsAdapter.getSortOrder() != currentSortOrder) {
+            mAllContactsAdapter.setSortOrder(currentSortOrder);
             changed = true;
         }
 
diff --git a/src/com/android/contacts/util/StreamItemEntry.java b/src/com/android/contacts/util/StreamItemEntry.java
index 6c8210f..46684e8 100644
--- a/src/com/android/contacts/util/StreamItemEntry.java
+++ b/src/com/android/contacts/util/StreamItemEntry.java
@@ -141,6 +141,12 @@
         return mPhotos;
     }
 
+    /**
+     * Make {@link #getDecodedText} and {@link #getDecodedComments} available.  Must be called
+     * before calling those.
+     *
+     * We can't do this automatically in the getters, because it'll require a {@link Context}.
+     */
     public void decodeHtml(Context context) {
         final Html.ImageGetter imageGetter = ContactDetailDisplayUtils.getImageGetter(context);
         if (mText != null) {
@@ -152,13 +158,21 @@
     }
 
     public CharSequence getDecodedText() {
+        checkDecoded(mText, mDecodedText);
         return mDecodedText;
     }
 
     public CharSequence getDecodedComments() {
+        checkDecoded(mComments, mDecodedComments);
         return mDecodedComments;
     }
 
+    private static void checkDecoded(CharSequence original, CharSequence decoded) {
+        if (original != null && decoded == null) {
+            throw new IllegalStateException("decodeHtml must have been called");
+        }
+    }
+
     private static String getString(Cursor cursor, String columnName) {
         return cursor.getString(cursor.getColumnIndex(columnName));
     }
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
index 04782fa..d80a35d 100644
--- a/tests/AndroidManifest.xml
+++ b/tests/AndroidManifest.xml
@@ -19,6 +19,8 @@
 
     <uses-permission android:name="android.permission.READ_CONTACTS" />
     <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+    <uses-permission android:name="android.permission.READ_CALL_LOG" />
+    <uses-permission android:name="android.permission.WRITE_CALL_LOG" />
     <uses-permission android:name="android.permission.GET_ACCOUNTS" />
 
     <uses-permission android:name="android.permission.USE_CREDENTIALS" />
diff --git a/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java b/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
index fd30390..419cac8 100644
--- a/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
+++ b/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
@@ -51,19 +51,20 @@
     }
 
     public void testAddStreamItemText_IncludesComments() {
-        StreamItemEntry streamItem = getTestBuilder().setComment("1 comment").build();
+        StreamItemEntry streamItem = getTestBuilder().setComment("1 comment").build(getContext());
         View streamItemView = addStreamItemText(streamItem);
         assertHasText(streamItemView, R.id.stream_item_comments, "1 comment");
     }
 
     public void testAddStreamItemText_IncludesHtmlComments() {
-        StreamItemEntry streamItem = getTestBuilder().setComment("1 <b>comment</b>").build();
+        StreamItemEntry streamItem = getTestBuilder().setComment("1 <b>comment</b>")
+                .build(getContext());
         View streamItemView = addStreamItemText(streamItem);
         assertHasHtmlText(streamItemView, R.id.stream_item_comments, "1 <b>comment<b>");
     }
 
     public void testAddStreamItemText_NoComments() {
-        StreamItemEntry streamItem = getTestBuilder().setComment(null).build();
+        StreamItemEntry streamItem = getTestBuilder().setComment(null).build(getContext());
         View streamItemView = addStreamItemText(streamItem);
         assertGone(streamItemView, R.id.stream_item_comments);
     }
diff --git a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
index 131af96..cd2d6bf 100644
--- a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
+++ b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
@@ -85,7 +85,7 @@
     private ArrayList<StreamItemEntry> createStreamItemList(int count) {
         ArrayList<StreamItemEntry> list = Lists.newArrayList();
         for (int index = 0; index < count; ++index) {
-            list.add(createStreamItemEntryBuilder().build());
+            list.add(createStreamItemEntryBuilder().build(getContext()));
         }
         return list;
     }
diff --git a/tests/src/com/android/contacts/format/SpannedTestUtils.java b/tests/src/com/android/contacts/format/SpannedTestUtils.java
index 646a7ec..ce228a7 100644
--- a/tests/src/com/android/contacts/format/SpannedTestUtils.java
+++ b/tests/src/com/android/contacts/format/SpannedTestUtils.java
@@ -41,7 +41,7 @@
             // If the text is empty, it does not add the <p></p> bits to it.
             Assert.assertEquals("", actualHtmlText);
         } else {
-            Assert.assertEquals("<p>" + expectedHtmlText + "</p>\n", actualHtmlText);
+            Assert.assertEquals("<p dir=ltr>" + expectedHtmlText + "</p>\n", actualHtmlText);
         }
     }
 
diff --git a/tests/src/com/android/contacts/util/StreamItemEntryBuilder.java b/tests/src/com/android/contacts/util/StreamItemEntryBuilder.java
index 319ba48..7fd9307 100644
--- a/tests/src/com/android/contacts/util/StreamItemEntryBuilder.java
+++ b/tests/src/com/android/contacts/util/StreamItemEntryBuilder.java
@@ -16,6 +16,10 @@
 
 package com.android.contacts.util;
 
+import com.android.contacts.util.StreamItemEntry;
+
+import android.content.Context;
+
 /**
  * Builder for {@link StreamItemEntry}s to make writing tests easier.
  */
@@ -58,8 +62,10 @@
         return this;
     }
 
-    public StreamItemEntry build() {
-        return new StreamItemEntry(mId, mText, mComment, mTimestamp, mAccountType, mAccountName,
-                mDataSet, mResPackage, mIconRes, mLabelRes);
+    public StreamItemEntry build(Context context) {
+        StreamItemEntry ret = new StreamItemEntry(mId, mText, mComment, mTimestamp, mAccountType,
+                mAccountName, mDataSet, mResPackage, mIconRes, mLabelRes);
+        ret.decodeHtml(context);
+        return ret;
     }
 }
\ No newline at end of file