Merge "Stream items UI."
diff --git a/res/layout-sw580dp-w1000dp/contact_detail_container.xml b/res/layout-sw580dp-w1000dp/contact_detail_container.xml
index b6a6319..be91296 100644
--- a/res/layout-sw580dp-w1000dp/contact_detail_container.xml
+++ b/res/layout-sw580dp-w1000dp/contact_detail_container.xml
@@ -30,12 +30,11 @@
         android:id="@+id/about_fragment"
         android:layout_width="0dip"
         android:layout_height="match_parent"
-        android:layout_weight="3" />
+        android:layout_weight="1" />
 
     <fragment class="com.android.contacts.detail.ContactDetailUpdatesFragment"
         android:id="@+id/updates_fragment"
-        android:layout_width="0dip"
-        android:layout_height="match_parent"
-        android:layout_weight="2" />
+        android:layout_width="306dip"
+        android:layout_height="match_parent" />
 
 </LinearLayout>
\ No newline at end of file
diff --git a/res/layout-sw580dp/people_activity.xml b/res/layout-sw580dp/people_activity.xml
index d3d7c32..87bb3b5 100644
--- a/res/layout-sw580dp/people_activity.xml
+++ b/res/layout-sw580dp/people_activity.xml
@@ -69,7 +69,7 @@
             ex:layout_narrowParentWidth="800dip"
             ex:layout_narrowMarginRight="0dip"
             ex:layout_wideParentWidth="1280dip"
-            ex:layout_wideMarginRight="48dip"
+            ex:layout_wideMarginRight="0dip"
             ex:clipMarginLeft="0dip"
             ex:clipMarginTop="3dip"
             ex:clipMarginRight="3dip"
diff --git a/res/layout/carousel_updates_tab.xml b/res/layout/carousel_updates_tab.xml
index d235280..d453dae 100644
--- a/res/layout/carousel_updates_tab.xml
+++ b/res/layout/carousel_updates_tab.xml
@@ -21,13 +21,22 @@
     android:layout_weight="1"
     android:background="@color/detail_tab_background">
 
-    <!-- Transparent view to overlay on the contact's photo
+    <ImageView android:id="@+id/status_photo"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"
+        android:visibility="gone" />
+
+
+    <!-- Transparent view to overlay on the update photo
     (to allow white text to appear over a white photo). -->
     <View
         android:layout_width="match_parent"
         android:layout_height="@dimen/detail_tab_carousel_tab_label_height"
         android:layout_alignParentLeft="true"
         android:layout_alignParentBottom="true"
+        android:layout_above="@id/status_photo"
         android:background="@android:color/black"
         android:alpha=".25"/>
 
@@ -37,6 +46,7 @@
         android:layout_height="@dimen/detail_tab_carousel_tab_label_height"
         android:layout_alignParentLeft="true"
         android:layout_alignParentBottom="true"
+        android:layout_above="@id/status_photo"
         android:paddingLeft="@dimen/detail_item_side_margin"
         android:singleLine="true"
         android:gravity="left|center_vertical"
diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml
index 36a40c3..92f3575 100644
--- a/res/layout/contact_detail_updates_fragment.xml
+++ b/res/layout/contact_detail_updates_fragment.xml
@@ -19,40 +19,31 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <!--
-      TODO: Make this a list of social updates instead of just showing one update. Wait
-      until the social integration is done in the provider so that we know the
-      best way to setup the adapter.
-    -->
-    <LinearLayout
-        android:orientation="vertical"
+    <ScrollView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:paddingTop="@dimen/detail_tab_carousel_height"
-        android:background="@color/background_primary">
+        android:background="@color/background_social_updates">
 
-        <include
-            android:id="@+id/title"
-            layout="@layout/contact_detail_kind_title_entry_view"
-            android:paddingTop="@dimen/detail_item_vertical_margin" />
-
-        <TextView android:id="@+id/status"
-            android:layout_width="wrap_content"
+        <LinearLayout
+            android:orientation="vertical"
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:paddingTop="@dimen/detail_item_vertical_margin"
-            android:paddingLeft="@dimen/detail_item_side_margin"
-            android:paddingRight="@dimen/detail_item_side_margin"
-            android:textAppearance="?android:attr/textAppearanceMedium"/>
+            android:paddingTop="@dimen/detail_update_section_top_padding">
 
-        <TextView android:id="@+id/status_date"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:paddingLeft="@dimen/detail_item_side_margin"
-            android:paddingRight="@dimen/detail_item_side_margin"
-            android:textAppearance="?android:attr/textAppearanceSmall"
-            android:textColor="?android:attr/textColorTertiary"/>
+            <include
+                android:id="@+id/title"
+                layout="@layout/contact_detail_kind_title_entry_view" />
 
-    </LinearLayout>
+            <LinearLayout
+                android:id="@+id/update_list"
+                android:orientation="vertical"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingLeft="@dimen/detail_update_section_side_padding"
+                android:paddingRight="@dimen/detail_update_section_side_padding" />
+        </LinearLayout>
+    </ScrollView>
 
     <View
         android:id="@+id/alpha_overlay"
diff --git a/res/layout/stream_item_one_column.xml b/res/layout/stream_item_one_column.xml
new file mode 100644
index 0000000..014e3f1
--- /dev/null
+++ b/res/layout/stream_item_one_column.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="@dimen/detail_update_section_item_vertical_padding"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:id="@+id/stream_item_content"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:paddingBottom="@dimen/detail_update_section_item_vertical_padding"
+        android:paddingLeft="@dimen/detail_update_section_item_left_padding"
+        android:orientation="vertical" />
+
+    <View
+        android:id="@+id/horizontal_divider"
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:background="?android:attr/dividerHorizontal" />
+</LinearLayout>
diff --git a/res/layout/stream_item_pair.xml b/res/layout/stream_item_pair.xml
new file mode 100644
index 0000000..61b248c
--- /dev/null
+++ b/res/layout/stream_item_pair.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="horizontal"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <FrameLayout android:id="@+id/stream_pair_first"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1" />
+
+    <FrameLayout android:id="@+id/stream_pair_second"
+        android:paddingLeft="@dimen/detail_update_section_internal_padding"
+        android:layout_width="0dip"
+        android:layout_height="wrap_content"
+        android:layout_weight="1" />
+</LinearLayout>
\ No newline at end of file
diff --git a/res/layout/stream_item_text.xml b/res/layout/stream_item_text.xml
new file mode 100644
index 0000000..601dfb9
--- /dev/null
+++ b/res/layout/stream_item_text.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <TextView android:id="@+id/stream_item_html"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="16sp"
+        android:textColor="@color/social_update_text_color" />
+
+    <TextView android:id="@+id/stream_item_attribution"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textAppearance="?android:attr/textAppearanceSmall"
+        android:textColor="@color/social_update_attribution_color"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/res/values-sw580dp-w1000dp/dimens.xml b/res/values-sw580dp-w1000dp/dimens.xml
index dfa1b81..f08abae 100644
--- a/res/values-sw580dp-w1000dp/dimens.xml
+++ b/res/values-sw580dp-w1000dp/dimens.xml
@@ -22,4 +22,5 @@
     <dimen name="detail_header_view_margin">16dip</dimen>
     <dimen name="detail_header_attribution_height">56dip</dimen>
     <dimen name="detail_tab_carousel_height">0dip</dimen>
+    <dimen name="detail_update_section_top_padding">48dip</dimen>
 </resources>
diff --git a/res/values-sw580dp/dimens.xml b/res/values-sw580dp/dimens.xml
index bcaf1d2..e671229 100644
--- a/res/values-sw580dp/dimens.xml
+++ b/res/values-sw580dp/dimens.xml
@@ -33,5 +33,6 @@
     <dimen name="list_section_height">37dip</dimen>
     <dimen name="directory_header_height">56dip</dimen>
     <dimen name="detail_tab_carousel_height">256dip</dimen>
+    <dimen name="detail_update_section_item_vertical_padding">32dip</dimen>
     <dimen name="search_view_width">400dip</dimen>
 </resources>
diff --git a/res/values-w470dp/strings.xml b/res/values-w470dp/strings.xml
new file mode 100644
index 0000000..8dc9c09
--- /dev/null
+++ b/res/values-w470dp/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string name="recent_updates">Updates</string>
+</resources>
\ No newline at end of file
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 7f4e237..8a644b1 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -29,6 +29,12 @@
 
     <color name="translucent_search_background">#cc000000</color>
 
+    <color name="background_social_updates">#ffeeeeee</color>
+
+    <color name="social_update_text_color">#ff333333</color>
+
+    <color name="social_update_attribution_color">#ff777777</color>
+
     <!-- Color used for the letter in the A-Z section header -->
     <color name="section_header_text_color">#ff999999</color>
 
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index c3b1fee..8d19a3e 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -104,6 +104,21 @@
     <!-- Left and right padding of the text within the update tab in the tab carousel -->
     <dimen name="detail_update_tab_side_padding">24dip</dimen>
 
+    <!-- Top padding of the update section in the contact detail card -->
+    <dimen name="detail_update_section_top_padding">8dip</dimen>
+
+    <!-- Left and right padding of the update section in the contact detail card -->
+    <dimen name="detail_update_section_side_padding">16dip</dimen>
+
+    <!-- Vertical padding above and below individual stream items -->
+    <dimen name="detail_update_section_item_vertical_padding">16dip</dimen>
+
+    <!-- Left-side padding for individual stream items -->
+    <dimen name="detail_update_section_item_left_padding">8dip</dimen>
+
+    <!-- Horizontal padding between content sections within a stream item -->
+    <dimen name="detail_update_section_internal_padding">16dip</dimen>
+
     <!-- Margin around the contact's photo on the contact card -->
     <dimen name="detail_contact_photo_margin">15dip</dimen>
 
diff --git a/res/values/strings.xml b/res/values/strings.xml
index bf9d403..87307ae 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -86,10 +86,10 @@
          creating a new group. This string represents the built in way to create the group. [CHAR LIMIT=NONE] -->
     <string name="insertGroupDescription">Create group</string>
 
-    <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] -->
+    <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=15] -->
     <string name="contactDetailAbout">About</string>
 
-    <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=11] -->
+    <!-- The tab label for the contact detail activity that displays information about the contact [CHAR LIMIT=15] -->
     <string name="contactDetailUpdates">Updates</string>
 
     <!-- Hint text in the search box when the user hits the Search key while in the contacts app -->
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index f10e6b8..fa0ffb2 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -17,6 +17,8 @@
 package com.android.contacts;
 
 import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.StreamItemEntry;
+import com.android.contacts.util.StreamItemPhotoEntry;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.content.ContentResolver;
@@ -42,6 +44,8 @@
 import android.provider.ContactsContract.DisplayNameSources;
 import android.provider.ContactsContract.Groups;
 import android.provider.ContactsContract.RawContacts;
+import android.provider.ContactsContract.StreamItems;
+import android.provider.ContactsContract.StreamItemPhotos;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -51,8 +55,12 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Loads a single Contact and all it constituent RawContacts.
@@ -62,6 +70,7 @@
 
     private Uri mLookupUri;
     private boolean mLoadGroupMetaData;
+    private boolean mLoadStreamItems;
     private Result mContact;
     private ForceLoadContentObserver mObserver;
     private boolean mDestroyed;
@@ -102,11 +111,8 @@
         private final boolean mStarred;
         private final Integer mPresence;
         private final ArrayList<Entity> mEntities;
+        private ArrayList<StreamItemEntry> mStreamItems;
         private final HashMap<Long, DataStatus> mStatuses;
-        private final String mStatus;
-        private final Long mStatusTimestamp;
-        private final Integer mStatusLabel;
-        private final String mStatusResPackage;
 
         private String mDirectoryDisplayName;
         private String mDirectoryType;
@@ -130,6 +136,7 @@
             mLookupKey = null;
             mId = -1;
             mEntities = null;
+            mStreamItems = new ArrayList<StreamItemEntry>();
             mStatuses = null;
             mNameRawContactId = -1;
             mDisplayNameSource = DisplayNameSources.UNDEFINED;
@@ -140,10 +147,6 @@
             mPhoneticName = null;
             mStarred = false;
             mPresence = null;
-            mStatus = null;
-            mStatusTimestamp = null;
-            mStatusLabel = null;
-            mStatusResPackage = null;
         }
 
         /**
@@ -152,14 +155,14 @@
         private Result(Uri uri, Uri lookupUri, long directoryId, String lookupKey, long id,
                 long nameRawContactId, int displayNameSource, long photoId, String photoUri,
                 String displayName, String altDisplayName, String phoneticName, boolean starred,
-                Integer presence, String status, Long statusTimestamp, Integer statusLabel,
-                String statusResPackage) {
+                Integer presence) {
             mLookupUri = lookupUri;
             mUri = uri;
             mDirectoryId = directoryId;
             mLookupKey = lookupKey;
             mId = id;
             mEntities = new ArrayList<Entity>();
+            mStreamItems = new ArrayList<StreamItemEntry>();
             mStatuses = new HashMap<Long, DataStatus>();
             mNameRawContactId = nameRawContactId;
             mDisplayNameSource = displayNameSource;
@@ -170,10 +173,6 @@
             mPhoneticName = phoneticName;
             mStarred = starred;
             mPresence = presence;
-            mStatus = status;
-            mStatusTimestamp = statusTimestamp;
-            mStatusLabel = statusLabel;
-            mStatusResPackage = statusResPackage;
         }
 
         private Result(Result from) {
@@ -192,11 +191,8 @@
             mStarred = from.mStarred;
             mPresence = from.mPresence;
             mEntities = from.mEntities;
+            mStreamItems = from.mStreamItems;
             mStatuses = from.mStatuses;
-            mStatus = from.mStatus;
-            mStatusTimestamp = from.mStatusTimestamp;
-            mStatusLabel = from.mStatusLabel;
-            mStatusResPackage = from.mStatusResPackage;
 
             mDirectoryDisplayName = from.mDirectoryDisplayName;
             mDirectoryType = from.mDirectoryType;
@@ -283,26 +279,14 @@
             return mPresence;
         }
 
-        public String getSocialSnippet() {
-            return mStatus;
-        }
-
-        public Long getStatusTimestamp() {
-            return mStatusTimestamp;
-        }
-
-        public Integer getStatusLabel() {
-            return mStatusLabel;
-        }
-
-        public String getStatusResPackage() {
-            return mStatusResPackage;
-        }
-
         public ArrayList<Entity> getEntities() {
             return mEntities;
         }
 
+        public ArrayList<StreamItemEntry> getStreamItems() {
+            return mStreamItems;
+        }
+
         public HashMap<Long, DataStatus> getStatuses() {
             return mStatuses;
         }
@@ -387,8 +371,11 @@
         }
     }
 
+    /**
+     * Projection used for the query that loads all data for the entire contact (except for
+     * social stream items).
+     */
     private static class ContactQuery {
-        // Projection used for the query that loads all data for the entire contact.
         final static String[] COLUMNS = new String[] {
                 Contacts.NAME_RAW_CONTACT_ID,
                 Contacts.DISPLAY_NAME_SOURCE,
@@ -524,8 +511,10 @@
         public final static int PHOTO_URI = 59;
     }
 
+    /**
+     * Projection used for the query that loads all data for the entire contact.
+     */
     private static class DirectoryQuery {
-        // Projection used for the query that loads all data for the entire contact.
         final static String[] COLUMNS = new String[] {
             Directory.DISPLAY_NAME,
             Directory.PACKAGE_NAME,
@@ -575,6 +564,9 @@
                     } else if (mLoadGroupMetaData) {
                         loadGroupMetaData(result);
                     }
+                    if (mLoadStreamItems) {
+                        loadStreamItems(result);
+                    }
                     loadPhotoBinaryData(result);
                 }
                 return result;
@@ -748,15 +740,6 @@
             final Integer presence = cursor.isNull(ContactQuery.CONTACT_PRESENCE)
                     ? null
                     : cursor.getInt(ContactQuery.CONTACT_PRESENCE);
-            final String status = cursor.getString(ContactQuery.CONTACT_STATUS);
-            final Long statusTimestamp = cursor.isNull(ContactQuery.CONTACT_STATUS_TIMESTAMP)
-                    ? null
-                    : cursor.getLong(ContactQuery.CONTACT_STATUS_TIMESTAMP);
-            final Integer statusLabel = cursor.isNull(ContactQuery.CONTACT_STATUS_LABEL)
-                    ? null
-                    : cursor.getInt(ContactQuery.CONTACT_STATUS_LABEL);
-            final String statusResPackage = cursor.getString(
-                    ContactQuery.CONTACT_STATUS_RES_PACKAGE);
 
             Uri lookupUri;
             if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
@@ -768,8 +751,7 @@
 
             return new Result(contactUri, lookupUri, directoryId, lookupKey, contactId,
                     nameRawContactId, displayNameSource, photoId, photoUri, displayName,
-                    altDisplayName, phoneticName, starred, presence, status, statusTimestamp,
-                    statusLabel, statusResPackage);
+                    altDisplayName, phoneticName, starred, presence);
         }
 
         /**
@@ -937,6 +919,60 @@
             }
         }
 
+        /**
+         * Loads all stream items and stream item photos belonging to this contact.
+         */
+        private void loadStreamItems(Result result) {
+            Cursor cursor = getContext().getContentResolver().query(
+                    Contacts.CONTENT_LOOKUP_URI.buildUpon()
+                            .appendPath(result.getLookupKey())
+                            .appendPath(Contacts.StreamItems.CONTENT_DIRECTORY).build(),
+                    null, null, null, null);
+            Map<Long, StreamItemEntry> streamItemsById = new HashMap<Long, StreamItemEntry>();
+            ArrayList<StreamItemEntry> streamItems = new ArrayList<StreamItemEntry>();
+            try {
+                while (cursor.moveToNext()) {
+                    StreamItemEntry streamItem = new StreamItemEntry(cursor);
+                    streamItemsById.put(streamItem.getId(), streamItem);
+                    streamItems.add(streamItem);
+                }
+            } finally {
+                cursor.close();
+            }
+
+            // Now retrieve any photo records associated with the stream items.
+            String[] streamItemIdArr = new String[streamItems.size()];
+            StringBuilder streamItemPhotoSelection = new StringBuilder();
+            if (!streamItems.isEmpty()) {
+                streamItemPhotoSelection.append(StreamItemPhotos.STREAM_ITEM_ID + " IN (");
+                for (int i = 0; i < streamItems.size(); i++) {
+                    if (i > 0) {
+                        streamItemPhotoSelection.append(",");
+                    }
+                    streamItemPhotoSelection.append("?");
+                    streamItemIdArr[i] = String.valueOf(streamItems.get(i).getId());
+                }
+                streamItemPhotoSelection.append(")");
+                cursor = getContext().getContentResolver().query(StreamItems.CONTENT_PHOTO_URI,
+                        null, streamItemPhotoSelection.toString(), streamItemIdArr,
+                        StreamItemPhotos.STREAM_ITEM_ID);
+                try {
+                    while (cursor.moveToNext()) {
+                        long streamItemId = cursor.getLong(
+                                cursor.getColumnIndex(StreamItemPhotos.STREAM_ITEM_ID));
+                        StreamItemEntry streamItem = streamItemsById.get(streamItemId);
+                        streamItem.addPhoto(new StreamItemPhotoEntry(cursor));
+                    }
+                } finally {
+                    cursor.close();
+                }
+            }
+
+            // Set the sorted stream items on the result.
+            Collections.sort(streamItems);
+            result.mStreamItems.addAll(streamItems);
+        }
+
         @Override
         protected void onPostExecute(Result result) {
             unregisterObserver();
@@ -1022,13 +1058,15 @@
     }
 
     public ContactLoader(Context context, Uri lookupUri) {
-        this(context, lookupUri, false);
+        this(context, lookupUri, false, false);
     }
 
-    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData) {
+    public ContactLoader(Context context, Uri lookupUri, boolean loadGroupMetaData,
+            boolean loadStreamItems) {
         super(context);
         mLookupUri = lookupUri;
         mLoadGroupMetaData = loadGroupMetaData;
+        mLoadStreamItems = loadStreamItems;
     }
 
     public Uri getLookupUri() {
diff --git a/src/com/android/contacts/activities/ContactDetailActivity.java b/src/com/android/contacts/activities/ContactDetailActivity.java
index 14378d9..4d04ac2 100644
--- a/src/com/android/contacts/activities/ContactDetailActivity.java
+++ b/src/com/android/contacts/activities/ContactDetailActivity.java
@@ -283,7 +283,7 @@
                 public void run() {
                     mContactData = result;
                     mLookupUri = result.getLookupUri();
-                    mContactHasUpdates = result.getSocialSnippet() != null;
+                    mContactHasUpdates = !result.getStreamItems().isEmpty();
                     invalidateOptionsMenu();
                     setupTitle();
                     if (mContactHasUpdates) {
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index 3f28b58..e9d75ef 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -18,10 +18,13 @@
 
 import com.android.contacts.ContactLoader;
 import com.android.contacts.ContactLoader.Result;
+import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.R;
 import com.android.contacts.format.FormatUtils;
 import com.android.contacts.preference.ContactsPreferences;
 import com.android.contacts.util.ContactBadgeUtil;
+import com.android.contacts.util.StreamItemEntry;
+import com.android.contacts.util.StreamItemPhotoEntry;
 
 import android.content.ContentValues;
 import android.content.Context;
@@ -30,19 +33,26 @@
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.graphics.Typeface;
+import android.net.Uri;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.CommonDataKinds.Organization;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.DisplayNameSources;
+import android.text.Html;
 import android.text.Spanned;
 import android.text.TextUtils;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AlphaAnimation;
 import android.widget.CheckBox;
 import android.widget.ImageView;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import java.util.List;
+
 /**
  * This class contains utility methods to bind high-level contact details
  * (meaning name, phonetic name, job, and attribution) from a
@@ -207,23 +217,135 @@
     /**
      * Set the social snippet text. If there isn't one, then set the view to gone.
      */
-    public static void setSocialSnippet(Context context, Result contactData, TextView statusView) {
+    public static void setSocialSnippet(Context context, Result contactData, TextView statusView,
+            ImageView statusPhotoView) {
         if (statusView == null) {
             return;
         }
-        setDataOrHideIfNone(contactData.getSocialSnippet(), statusView);
+
+        String snippet = null;
+        String photoUri = null;
+        if (!contactData.getStreamItems().isEmpty()) {
+            StreamItemEntry firstEntry = contactData.getStreamItems().get(0);
+            snippet = firstEntry.getText();
+            if (!firstEntry.getPhotos().isEmpty()) {
+                StreamItemPhotoEntry firstPhoto = firstEntry.getPhotos().get(0);
+                photoUri = firstPhoto.getPhotoUri();
+
+                // If displaying an image, hide the snippet text.
+                snippet = null;
+            }
+        }
+        setDataOrHideIfNone(snippet, statusView);
+        if (photoUri != null) {
+            ContactPhotoManager.getInstance(context).loadPhoto(
+                    statusPhotoView, Uri.parse(photoUri));
+            statusPhotoView.setVisibility(View.VISIBLE);
+        } else {
+            statusPhotoView.setVisibility(View.GONE);
+        }
     }
 
     /**
-     * Set the social snippet text and date. If there isn't one, then set the view to gone.
+     * Displays the social stream items under the given layout.
      */
-    public static void setSocialSnippetAndDate(Context context, Result contactData,
-            TextView statusView, TextView dateView) {
-        if (statusView == null || dateView == null) {
-            return;
+    public static void showSocialStreamItems(LayoutInflater inflater, Context context,
+            Result contactData, LinearLayout streamContainer) {
+        if (streamContainer != null) {
+            streamContainer.removeAllViews();
+            List<StreamItemEntry> streamItems = contactData.getStreamItems();
+            for (StreamItemEntry streamItem : streamItems) {
+                addStreamItemToContainer(inflater, context, streamItem, streamContainer);
+            }
         }
-        setDataOrHideIfNone(contactData.getSocialSnippet(), statusView);
-        setDataOrHideIfNone(ContactBadgeUtil.getSocialDate(contactData, context), dateView);
+    }
+
+    public static void addStreamItemToContainer(LayoutInflater inflater, Context context,
+            StreamItemEntry streamItem, LinearLayout streamContainer) {
+        View oneColumnView = inflater.inflate(R.layout.stream_item_one_column,
+                streamContainer, false);
+        ViewGroup contentBox = (ViewGroup) oneColumnView.findViewById(R.id.stream_item_content);
+        int internalPadding = context.getResources().getDimensionPixelSize(
+                R.dimen.detail_update_section_internal_padding);
+
+        // TODO: This is not the correct layout for a stream item with photos.  Photos should be
+        // displayed first, then the update text either to the right of the final image (if there
+        // are an odd number of images) or below the last row of images (if there are an even
+        // number of images).  Since this is designed as a two-column grid, we should also consider
+        // using a TableLayout instead of the series of nested LinearLayouts that we have now.
+        // See the Updates section of the Contacts Architecture document for details.
+
+        // If there are no photos, just display the text in a single column.
+        List<StreamItemPhotoEntry> photos = streamItem.getPhotos();
+        if (photos.isEmpty()) {
+            addStreamItemText(inflater, context, streamItem, contentBox);
+        } else {
+            // If the first photo is square or portrait mode, show the text alongside it.
+            boolean isFirstPhotoAlongsideText = false;
+            StreamItemPhotoEntry firstPhoto = photos.get(0);
+            isFirstPhotoAlongsideText = firstPhoto.getHeight() >= firstPhoto.getWidth();
+            if (isFirstPhotoAlongsideText) {
+                View twoColumnView = inflater.inflate(R.layout.stream_item_pair, contentBox, false);
+                addStreamItemPhoto(inflater, context, firstPhoto,
+                        (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_first));
+                addStreamItemText(inflater, context, streamItem,
+                        (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_second));
+                contentBox.addView(twoColumnView);
+            } else {
+                // Just add the stream item text at the top of the entry.
+                addStreamItemText(inflater, context, streamItem, contentBox);
+            }
+            for (int i = isFirstPhotoAlongsideText ? 1 : 0; i < photos.size(); i++) {
+                StreamItemPhotoEntry photo = photos.get(i);
+
+                // If the photo is landscape, show it at full-width.
+                if (photo.getWidth() > photo.getHeight()) {
+                    View photoView = addStreamItemPhoto(inflater, context, photo, contentBox);
+                    photoView.setPadding(0, internalPadding, 0, 0);
+                } else {
+                    // If this photo and the next are both square or portrait, show them as a pair.
+                    StreamItemPhotoEntry nextPhoto = i + 1 < photos.size()
+                            ? photos.get(i + 1) : null;
+                    if (nextPhoto != null && nextPhoto.getHeight() >= nextPhoto.getWidth()) {
+                        View twoColumnView = inflater.inflate(R.layout.stream_item_pair,
+                                contentBox, false);
+                        addStreamItemPhoto(inflater, context, photo,
+                                (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_first));
+                        addStreamItemPhoto(inflater, context, nextPhoto,
+                                (ViewGroup) twoColumnView.findViewById(R.id.stream_pair_second));
+                        twoColumnView.setPadding(0, internalPadding, 0, 0);
+                        contentBox.addView(twoColumnView);
+                        i++;
+                    } else {
+                        View photoView = addStreamItemPhoto(inflater, context, photo, contentBox);
+                        photoView.setPadding(0, internalPadding, 0, 0);
+                    }
+                }
+            }
+        }
+
+        streamContainer.addView(oneColumnView);
+    }
+
+    private static View addStreamItemText(LayoutInflater inflater, Context context,
+            StreamItemEntry streamItem, ViewGroup parent) {
+        View textUpdate = inflater.inflate(R.layout.stream_item_text, parent, false);
+        TextView htmlView = (TextView) textUpdate.findViewById(R.id.stream_item_html);
+        TextView attributionView = (TextView) textUpdate.findViewById(
+                R.id.stream_item_attribution);
+        htmlView.setText(Html.fromHtml(streamItem.getText()));
+        attributionView.setText(ContactBadgeUtil.getSocialDate(streamItem, context));
+        parent.addView(textUpdate);
+        return textUpdate;
+    }
+
+    private static View addStreamItemPhoto(LayoutInflater inflater, Context context,
+            StreamItemPhotoEntry streamItemPhoto, ViewGroup parent) {
+        ImageView image = new ImageView(context);
+        ContactPhotoManager.getInstance(context).loadPhoto(
+                image, Uri.parse(streamItemPhoto.getPhotoUri()));
+        parent.addView(image);
+        return image;
     }
 
     /**
diff --git a/src/com/android/contacts/detail/ContactDetailHeaderView.java b/src/com/android/contacts/detail/ContactDetailHeaderView.java
deleted file mode 100644
index 63f8fbe..0000000
--- a/src/com/android/contacts/detail/ContactDetailHeaderView.java
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- * Copyright (C) 2010 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.detail;
-
-import com.android.contacts.ContactLoader;
-import com.android.contacts.ContactLoader.Result;
-import com.android.contacts.ContactSaveService;
-import com.android.contacts.R;
-import com.android.contacts.format.FormatUtils;
-import com.android.contacts.preference.ContactsPreferences;
-import com.android.contacts.util.ContactBadgeUtil;
-
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.ContentValues;
-import android.content.Context;
-import android.content.Entity;
-import android.content.Entity.NamedContentValues;
-import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Typeface;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Organization;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.DisplayNameSources;
-import android.text.Spanned;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.AlphaAnimation;
-import android.widget.CheckBox;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-/**
- * Header for displaying a title bar with contact info. You
- * can bind specific values by calling
- * {@link ContactDetailHeaderView#loadData(com.android.contacts.ContactLoader.Result)}
- * TODO: Refactor to use {@link ContactDetailDisplayUtils}
- */
-public class ContactDetailHeaderView extends FrameLayout
-        implements View.OnClickListener, View.OnLongClickListener {
-    private static final String TAG = "ContactDetailHeaderView";
-
-    private static final int PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS = 100;
-
-    private TextView mDisplayNameView;
-    private TextView mPhoneticNameView;
-    private TextView mOrganizationTextView;
-    private CheckBox mStarredView;
-    private ImageView mPhotoView;
-    private View mStatusContainerView;
-    private TextView mStatusView;
-    private TextView mStatusDateView;
-    private TextView mAttributionView;
-
-    private Uri mContactUri;
-    private Listener mListener;
-
-    /**
-     * Interface for callbacks invoked when the user interacts with a header.
-     */
-    public interface Listener {
-        public void onPhotoClick(View view);
-        public void onDisplayNameClick(View view);
-    }
-
-    public ContactDetailHeaderView(Context context) {
-        this(context, null);
-    }
-
-    public ContactDetailHeaderView(Context context, AttributeSet attrs) {
-        this(context, attrs, 0);
-    }
-
-    public ContactDetailHeaderView(Context context, AttributeSet attrs, int defStyle) {
-        super(context, attrs, defStyle);
-
-        final LayoutInflater inflater =
-            (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-        inflater.inflate(R.layout.contact_detail_header_view, this);
-
-        mDisplayNameView = (TextView) findViewById(R.id.name);
-        mDisplayNameView.setOnLongClickListener(this);
-
-        mPhoneticNameView = (TextView) findViewById(R.id.phonetic_name);
-        mPhoneticNameView.setOnLongClickListener(this);
-
-        mOrganizationTextView = (TextView) findViewById(R.id.organization);
-        mOrganizationTextView.setOnLongClickListener(this);
-
-        mStarredView = (CheckBox)findViewById(R.id.star);
-        mStarredView.setOnClickListener(this);
-
-        mPhotoView = (ImageView) findViewById(R.id.photo);
-
-        mStatusContainerView = findViewById(R.id.status_container);
-        mStatusView = (TextView)findViewById(R.id.status);
-        mStatusDateView = (TextView)findViewById(R.id.status_date);
-
-        mAttributionView = (TextView) findViewById(R.id.attribution);
-    }
-
-    /**
-     * Loads the data from the Loader-Result. This is the only function that has to be called
-     * from the outside to fully setup the View
-     */
-    public void loadData(ContactLoader.Result contactData) {
-        mContactUri = contactData.getLookupUri();
-
-        setDisplayName(contactData.getDisplayName(), contactData.getAltDisplayName(),
-                contactData.getPhoneticName());
-        setCompany(contactData);
-        if (contactData.isLoadingPhoto()) {
-            setPhoto(null, false);
-        } else {
-            byte[] photo = contactData.getPhotoBinaryData();
-            setPhoto(photo != null ? BitmapFactory.decodeByteArray(photo, 0, photo.length)
-                            : ContactBadgeUtil.loadPlaceholderPhoto(mContext),
-                    contactData.isDirectoryEntry());
-        }
-
-        setStared(!contactData.isDirectoryEntry(), contactData.getStarred());
-        setSocialSnippet(contactData.getSocialSnippet());
-        setSocialDate(ContactBadgeUtil.getSocialDate(contactData, getContext()));
-        setAttribution(contactData.getEntities().size() > 1, contactData.isDirectoryEntry(),
-                contactData.getDirectoryDisplayName(), contactData.getDirectoryType());
-    }
-
-    /**
-     * Set the given {@link Listener} to handle header events.
-     */
-    public void setListener(Listener listener) {
-        mListener = listener;
-    }
-
-    private void performPhotoClick() {
-        if (mListener != null) {
-            mListener.onPhotoClick(mPhotoView);
-        }
-    }
-
-    private void performDisplayNameClick() {
-        if (mListener != null) {
-            mListener.onDisplayNameClick(mDisplayNameView);
-        }
-    }
-
-    /**
-     * Set the starred state of this header widget.
-     */
-    private void setStared(boolean visible, boolean starred) {
-        if (visible) {
-            mStarredView.setVisibility(View.VISIBLE);
-            mStarredView.setChecked(starred);
-        } else {
-            mStarredView.setVisibility(View.GONE);
-        }
-    }
-
-    /**
-     * Set the photo to display in the header. If bitmap is null, the default placeholder
-     * image is shown
-     */
-    private void setPhoto(Bitmap bitmap, boolean fadeIn) {
-        if (mPhotoView.getDrawable() == null && fadeIn) {
-            AlphaAnimation animation = new AlphaAnimation(0, 1);
-            animation.setDuration(PHOTO_FADE_IN_ANIMATION_DURATION_MILLIS);
-            animation.setInterpolator(new AccelerateInterpolator());
-            mPhotoView.startAnimation(animation);
-        }
-        mPhotoView.setImageBitmap(bitmap);
-    }
-
-    /**
-     * Set the display name and phonetic name to show in the header.
-     */
-    private void setDisplayName(CharSequence displayName, CharSequence altDisplayName,
-            CharSequence phoneticName) {
-
-        // Check the preference for display name ordering, and bold the contact's first name if
-        // possible.
-        ContactsPreferences prefs = new ContactsPreferences(getContext());
-        CharSequence styledName = "";
-        if (!TextUtils.isEmpty(displayName) && !TextUtils.isEmpty(altDisplayName)) {
-            if (prefs.getDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
-                int overlapPoint = FormatUtils.overlapPoint(
-                        displayName.toString(), altDisplayName.toString());
-                if (overlapPoint > 0) {
-                    styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD,
-                            displayName, 0, overlapPoint, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                } else {
-                    styledName = displayName;
-                }
-            } else {
-                // Displaying alternate display name.
-                int overlapPoint = FormatUtils.overlapPoint(
-                        altDisplayName.toString(), displayName.toString());
-                if (overlapPoint > 0) {
-                    styledName = FormatUtils.applyStyleToSpan(Typeface.BOLD,
-                            altDisplayName, overlapPoint, altDisplayName.length(),
-                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-                } else {
-                    styledName = altDisplayName;
-                }
-            }
-        }
-        mDisplayNameView.setText(styledName);
-
-        if (TextUtils.isEmpty(phoneticName)) {
-            mPhoneticNameView.setVisibility(View.GONE);
-        } else {
-            mPhoneticNameView.setText(phoneticName);
-            mPhoneticNameView.setVisibility(View.VISIBLE);
-        }
-    }
-
-    /**
-     * Sets the organization info. If several organizations are given, the first one is used
-     */
-    private void setCompany(Result contactData) {
-        final boolean displayNameIsOrganization =
-            contactData.getDisplayNameSource() == DisplayNameSources.ORGANIZATION;
-        for (Entity entity : contactData.getEntities()) {
-            for (NamedContentValues subValue : entity.getSubValues()) {
-                final ContentValues entryValues = subValue.values;
-                final String mimeType = entryValues.getAsString(Data.MIMETYPE);
-
-                if (Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
-                    final String company = entryValues.getAsString(Organization.COMPANY);
-                    final String title = entryValues.getAsString(Organization.TITLE);
-                    final String combined;
-                    // We need to show company and title in a combined string. However, if the
-                    // DisplayName is already the organization, it mirrors company or (if company
-                    // is empty title). Make sure we don't show what's already shown as DisplayName
-                    if (TextUtils.isEmpty(company)) {
-                        combined = displayNameIsOrganization ? null : title;
-                    } else {
-                        if (TextUtils.isEmpty(title)) {
-                            combined = displayNameIsOrganization ? null : company;
-                        } else {
-                            if (displayNameIsOrganization) {
-                                combined = title;
-                            } else {
-                                combined = getResources().getString(
-                                        R.string.organization_company_and_title,
-                                        company, title);
-                            }
-                        }
-                    }
-
-                    if (TextUtils.isEmpty(combined)) {
-                        mOrganizationTextView.setVisibility(GONE);
-                    } else {
-                        mOrganizationTextView.setVisibility(VISIBLE);
-                        mOrganizationTextView.setText(combined);
-                    }
-
-                    return;
-                }
-            }
-        }
-        mOrganizationTextView.setVisibility(GONE);
-    }
-
-    /**
-     * Set the social snippet text to display in the header.
-     */
-    private void setSocialSnippet(CharSequence snippet) {
-        if (TextUtils.isEmpty(snippet)) {
-            // No status info. Hide everything
-            if (mStatusContainerView != null) mStatusContainerView.setVisibility(View.GONE);
-            mStatusView.setVisibility(View.GONE);
-            mStatusDateView.setVisibility(View.GONE);
-        } else {
-            // We have status info. Show the bubble
-            if (mStatusContainerView != null) mStatusContainerView.setVisibility(View.VISIBLE);
-            mStatusView.setVisibility(View.VISIBLE);
-            mStatusView.setText(snippet);
-        }
-    }
-
-    /**
-     * Set the status attribution text to display in the header.
-     */
-
-    private void setSocialDate(CharSequence dateText) {
-        if (TextUtils.isEmpty(dateText)) {
-            mStatusDateView.setVisibility(View.GONE);
-        } else {
-            mStatusDateView.setText(dateText);
-            mStatusDateView.setVisibility(View.VISIBLE);
-        }
-    }
-
-    private void setAttribution(boolean isJoinedContact, boolean isDirectoryEntry,
-            String directoryDisplayName, String directoryType) {
-        if (isJoinedContact) {
-            mAttributionView.setText(R.string.indicator_joined_contact);
-            mAttributionView.setVisibility(View.VISIBLE);
-        } else if (isDirectoryEntry) {
-            String displayName = !TextUtils.isEmpty(directoryDisplayName)
-                    ? directoryDisplayName
-                    : directoryType;
-            String text = getContext().getString(
-                    R.string.contact_directory_description, displayName);
-            mAttributionView.setText(text);
-            mAttributionView.setVisibility(View.VISIBLE);
-        } else {
-            mAttributionView.setVisibility(View.INVISIBLE);
-        }
-    }
-
-    @Override
-    public void onClick(View view) {
-        switch (view.getId()) {
-            case R.id.star: {
-                // Toggle "starred" state
-                // Make sure there is a contact
-                if (mContactUri != null) {
-                    Intent intent = ContactSaveService.createSetStarredIntent(
-                            getContext(), mContactUri, mStarredView.isChecked());
-                    getContext().startService(intent);
-                }
-                break;
-            }
-            case R.id.photo: {
-                performPhotoClick();
-                break;
-            }
-            case R.id.name: {
-                performDisplayNameClick();
-                break;
-            }
-        }
-    }
-
-    @Override
-    public boolean onLongClick(View v) {
-        if (!(v instanceof TextView)) {
-            return false;
-        }
-
-        CharSequence text = ((TextView)v).getText();
-
-        if (TextUtils.isEmpty(text)) {
-            return false;
-        }
-
-        ClipboardManager cm = (ClipboardManager) getContext().getSystemService(
-                Context.CLIPBOARD_SERVICE);
-        cm.setPrimaryClip(ClipData.newPlainText(null, text));
-        Toast.makeText(getContext(), R.string.toast_text_copied, Toast.LENGTH_SHORT).show();
-        return true;
-    }
-}
diff --git a/src/com/android/contacts/detail/ContactDetailLayoutController.java b/src/com/android/contacts/detail/ContactDetailLayoutController.java
index dda3884..d93edea 100644
--- a/src/com/android/contacts/detail/ContactDetailLayoutController.java
+++ b/src/com/android/contacts/detail/ContactDetailLayoutController.java
@@ -18,6 +18,7 @@
 
 import com.android.contacts.ContactLoader;
 import com.android.contacts.activities.PeopleActivity.ContactDetailFragmentListener;
+import com.android.contacts.util.StreamItemEntry;
 
 import android.app.Fragment;
 import android.app.FragmentManager;
@@ -124,7 +125,7 @@
 
     public void setContactData(ContactLoader.Result data) {
         mContactData = data;
-        if (mContactData.getSocialSnippet() != null) {
+        if (!data.getStreamItems().isEmpty()) {
             showContactWithUpdates();
         } else {
             showContactWithoutUpdates();
diff --git a/src/com/android/contacts/detail/ContactDetailTabCarousel.java b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
index 6e1b199..26987f6 100644
--- a/src/com/android/contacts/detail/ContactDetailTabCarousel.java
+++ b/src/com/android/contacts/detail/ContactDetailTabCarousel.java
@@ -45,6 +45,7 @@
 
     private ImageView mPhotoView;
     private TextView mStatusView;
+    private ImageView mStatusPhotoView;
 
     private Listener mListener;
 
@@ -115,6 +116,7 @@
 
         // Retrieve the social update views for the "updates" tab
         mStatusView = (TextView) updateView.findViewById(R.id.status);
+        mStatusPhotoView = (ImageView) updateView.findViewById(R.id.status_photo);
     }
 
     @Override
@@ -196,7 +198,8 @@
         }
 
         ContactDetailDisplayUtils.setPhoto(mContext, contactData, mPhotoView);
-        ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView);
+        ContactDetailDisplayUtils.setSocialSnippet(mContext, contactData, mStatusView,
+                mStatusPhotoView);
     }
 
     /**
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index 85dcc1d..daeae00 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -27,6 +27,7 @@
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
+import android.widget.LinearLayout;
 import android.widget.TextView;
 
 public class ContactDetailUpdatesFragment extends Fragment
@@ -37,8 +38,10 @@
     private ContactLoader.Result mContactData;
     private Uri mLookupUri;
 
-    private TextView mStatusView;
-    private TextView mStatusDateView;
+    private LayoutInflater mInflater;
+
+    // The linear layout that contains all the stream items.
+    private LinearLayout mStreamContainer;
 
     /**
      * This optional view adds an alpha layer over the entire fragment.
@@ -57,22 +60,22 @@
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
-        View rootView = inflater.inflate(R.layout.contact_detail_updates_fragment, container,
+        mInflater = inflater;
+        View rootView = mInflater.inflate(R.layout.contact_detail_updates_fragment, container,
                 false);
 
         TextView titleTextView = (TextView) rootView.findViewById(R.id.kind);
         titleTextView.setText(getString(R.string.recent_updates).toUpperCase());
 
-        mStatusView = (TextView) rootView.findViewById(R.id.status);
-        mStatusDateView = (TextView) rootView.findViewById(R.id.status_date);
+        mStreamContainer = (LinearLayout) rootView.findViewById(R.id.update_list);
 
         // It is possible that the contact data was set to the fragment when it was first attached
         // to the activity, but before this method was called because the fragment was not
         // visible on screen yet (i.e. using a {@link ViewPager}), so display the data if we already
         // have it.
         if (mContactData != null) {
-            ContactDetailDisplayUtils.setSocialSnippetAndDate(getActivity(), mContactData,
-                    mStatusView, mStatusDateView);
+            ContactDetailDisplayUtils.showSocialStreamItems(inflater, getActivity(), mContactData,
+                    mStreamContainer);
         }
 
         mAlphaLayer = rootView.findViewById(R.id.alpha_overlay);
@@ -87,8 +90,8 @@
         }
         mLookupUri = lookupUri;
         mContactData = result;
-        ContactDetailDisplayUtils.setSocialSnippetAndDate(getActivity(), mContactData,
-                mStatusView, mStatusDateView);
+        ContactDetailDisplayUtils.showSocialStreamItems(mInflater, getActivity(), mContactData,
+                mStreamContainer);
     }
 
     @Override
diff --git a/src/com/android/contacts/detail/ContactLoaderFragment.java b/src/com/android/contacts/detail/ContactLoaderFragment.java
index 0dc83ef..034a8cc 100644
--- a/src/com/android/contacts/detail/ContactLoaderFragment.java
+++ b/src/com/android/contacts/detail/ContactLoaderFragment.java
@@ -170,7 +170,8 @@
         @Override
         public Loader<ContactLoader.Result> onCreateLoader(int id, Bundle args) {
             Uri lookupUri = args.getParcelable(LOADER_ARG_CONTACT_URI);
-            return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */);
+            return new ContactLoader(mContext, lookupUri, true /* loadGroupMetaData */,
+                    true /* loadStreamItems */);
         }
 
         @Override
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
index 92118c0..d59aebd 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
@@ -20,6 +20,7 @@
 import com.android.contacts.R;
 import com.android.contacts.util.ContactBadgeUtil;
 import com.android.contacts.util.DataStatus;
+import com.android.contacts.util.StreamItemEntry;
 
 import android.app.PendingIntent;
 import android.appwidget.AppWidgetManager;
@@ -44,6 +45,7 @@
 import android.widget.RemoteViews;
 
 import java.util.HashMap;
+import java.util.List;
 
 public class SocialWidgetProvider extends AppWidgetProvider {
     private static final String TAG = "SocialWidgetProvider";
@@ -109,7 +111,7 @@
             // Not yet set-up (this can happen while the Configuration activity is visible)
             return;
         }
-        final ContactLoader contactLoader = new ContactLoader(context, contactUri);
+        final ContactLoader contactLoader = new ContactLoader(context, contactUri, false, true);
         contactLoader.registerListener(0,
                 new ContactLoader.OnLoadCompleteListener<ContactLoader.Result>() {
                     @Override
@@ -140,8 +142,13 @@
             setPhoto(views, photo != null
                     ? BitmapFactory.decodeByteArray(photo, 0, photo.length)
                             : ContactBadgeUtil.loadPlaceholderPhoto(context));
-            setStatusAttribution(views, ContactBadgeUtil.getSocialDate(
-                    contactData, context));
+
+            // TODO: Rotate between all the stream items?
+            StreamItemEntry streamItem = null;
+            if (!contactData.getStreamItems().isEmpty()) {
+                streamItem = contactData.getStreamItems().get(0);
+                setStatusAttribution(views, ContactBadgeUtil.getSocialDate(streamItem, context));
+            }
 
             // OnClick launch QuickContact
             final Intent intent = new Intent(QuickContact.ACTION_QUICK_CONTACT);
@@ -157,7 +164,7 @@
             views.setOnClickPendingIntent(R.id.border, pendingIntent);
 
             setDisplayNameAndSnippet(context, views, contactData.getDisplayName(),
-                    contactData.getPhoneticName(), contactData.getStatuses(), pendingIntent);
+                    contactData.getPhoneticName(), contactData.getStreamItems(), pendingIntent);
         }
 
         // Configure UI
@@ -174,7 +181,7 @@
      */
     private static void setDisplayNameAndSnippet(Context context, RemoteViews views,
             CharSequence displayName, CharSequence phoneticName,
-            HashMap<Long, DataStatus> statuses, PendingIntent defaultIntent) {
+            List<StreamItemEntry> streamItems, PendingIntent defaultIntent) {
         SpannableStringBuilder sb = new SpannableStringBuilder();
 
         CharSequence name = displayName;
@@ -190,27 +197,15 @@
         sb.setSpan(sizeSpan, 0, name.length(), 0);
         sb.setSpan(styleSpan, 0, name.length(), 0);
 
-        long latestStatusId = 0;
-        DataStatus latestStatus = null;
-        if (statuses != null) {
-            for (HashMap.Entry<Long, DataStatus> entry : statuses.entrySet()) {
-                DataStatus status = entry.getValue();
-                if (!TextUtils.isEmpty(status.getStatus())
-                        && (latestStatus == null
-                                || latestStatus.getTimestamp() < status.getTimestamp())) {
-                    latestStatusId = entry.getKey();
-                    latestStatus = status;
-                }
-            }
-        }
-
-        if (latestStatus == null) {
+        if (streamItems.isEmpty()) {
             views.setTextViewText(R.id.name, sb);
             views.setViewVisibility(R.id.name, View.VISIBLE);
             views.setViewVisibility(R.id.name_and_snippet, View.GONE);
             views.setOnClickPendingIntent(R.id.widget_container, defaultIntent);
         } else {
-            CharSequence status = latestStatus.getStatus();
+            // TODO: Rotate between all the stream items?
+            StreamItemEntry streamItem = streamItems.get(0);
+            CharSequence status = streamItem.getText();
             if (status.length() <= SHORT_SNIPPET_LENGTH) {
                 sb.append("\n");
             } else {
@@ -220,11 +215,13 @@
             views.setTextViewText(R.id.name_and_snippet, sb);
             views.setViewVisibility(R.id.name, View.GONE);
             views.setViewVisibility(R.id.name_and_snippet, View.VISIBLE);
-            final Intent intent = new Intent(Intent.ACTION_VIEW,
-                    ContentUris.withAppendedId(Data.CONTENT_URI, latestStatusId));
-
-            views.setOnClickPendingIntent(R.id.name_and_snippet_container,
-                    PendingIntent.getActivity(context, 0, intent, 0));
+            if (!TextUtils.isEmpty(streamItem.getAction())
+                    && !TextUtils.isEmpty(streamItem.getActionUri())) {
+                final Intent intent = new Intent(streamItem.getAction(),
+                        Uri.parse(streamItem.getActionUri()));
+                views.setOnClickPendingIntent(R.id.name_and_snippet_container,
+                        PendingIntent.getActivity(context, 0, intent, 0));
+            }
         }
     }
 
diff --git a/src/com/android/contacts/util/ContactBadgeUtil.java b/src/com/android/contacts/util/ContactBadgeUtil.java
index 65025f8..2f37b4c 100644
--- a/src/com/android/contacts/util/ContactBadgeUtil.java
+++ b/src/com/android/contacts/util/ContactBadgeUtil.java
@@ -37,17 +37,11 @@
     private static final String TAG = "ContactBadgeUtil";
 
     /**
-     * Returns the social snippet attribution, including the date
+     * Returns the social snippet attribution for the given stream item entry, including the date.
      */
-    public static CharSequence getSocialDate(ContactLoader.Result contactData,
-            Context context) {
-        if (TextUtils.isEmpty(contactData.getSocialSnippet())) {
-            return null;
-        }
-
+    public static CharSequence getSocialDate(StreamItemEntry streamItem, Context context) {
         final CharSequence timestampDisplayValue;
-
-        final Long statusTimestamp = contactData.getStatusTimestamp();
+        final Long statusTimestamp = streamItem.getTimestamp();
         if (statusTimestamp  != null) {
             // Set the date/time field by mixing relative and absolute
             // times.
@@ -63,8 +57,8 @@
 
         String labelDisplayValue = null;
 
-        final Integer statusLabel = contactData.getStatusLabel();
-        final String statusResPackage = contactData.getStatusResPackage();
+        final Integer statusLabel = streamItem.getLabelRes();
+        final String statusResPackage = streamItem.getResPackage();
         if (statusLabel  != null) {
             Resources resources;
             if (TextUtils.isEmpty(statusResPackage)) {
diff --git a/src/com/android/contacts/util/StreamItemEntry.java b/src/com/android/contacts/util/StreamItemEntry.java
new file mode 100644
index 0000000..dc54229
--- /dev/null
+++ b/src/com/android/contacts/util/StreamItemEntry.java
@@ -0,0 +1,141 @@
+/*
+ * 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 android.database.Cursor;
+import android.provider.ContactsContract.StreamItems;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Data object for a social stream item.  Social stream items may contain multiple
+ * mPhotos.  Social stream item entries are comparable; entries with more recent
+ * timestamps will be displayed on top.
+ */
+public class StreamItemEntry implements Comparable<StreamItemEntry> {
+
+    // Basic stream item fields.
+    private final long mId;
+    private final String mText;
+    private final String mComments;
+    private final long mTimestamp;
+    private final String mAction;
+    private final String mActionUri;
+
+    // Package references for label and icon resources.
+    private final String mResPackage;
+    private final int mIconRes;
+    private final int mLabelRes;
+
+    // Photos associated with this stream item.
+    private List<StreamItemPhotoEntry> mPhotos;
+
+    public StreamItemEntry(long id, String text, String comments, long timestamp, String action,
+            String actionUri, String resPackage, int iconRes, int labelRes) {
+        mId = id;
+        mText = text;
+        mComments = comments;
+        mTimestamp = timestamp;
+        mAction = action;
+        mActionUri = actionUri;
+        mResPackage = resPackage;
+        mIconRes = iconRes;
+        mLabelRes = labelRes;
+        mPhotos = new ArrayList<StreamItemPhotoEntry>();
+    }
+
+    public StreamItemEntry(Cursor cursor) {
+        // This is expected to be populated via a cursor containing all StreamItems columns in
+        // its projection.
+        mId = getLong(cursor, StreamItems._ID);
+        mText = getString(cursor, StreamItems.TEXT);
+        mComments = getString(cursor, StreamItems.COMMENTS);
+        mTimestamp = getLong(cursor, StreamItems.TIMESTAMP);
+        mAction = getString(cursor, StreamItems.ACTION);
+        mActionUri = getString(cursor, StreamItems.ACTION_URI);
+        mResPackage = getString(cursor, StreamItems.RES_PACKAGE);
+        mIconRes = getInt(cursor, StreamItems.RES_ICON, -1);
+        mLabelRes = getInt(cursor, StreamItems.RES_LABEL, -1);
+        mPhotos = new ArrayList<StreamItemPhotoEntry>();
+    }
+
+    public void addPhoto(StreamItemPhotoEntry photoEntry) {
+        mPhotos.add(photoEntry);
+    }
+
+    @Override
+    public int compareTo(StreamItemEntry other) {
+        return mTimestamp == other.mTimestamp ? 0 : mTimestamp > other.mTimestamp ? -1 : 1;
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public String getComments() {
+        return mComments;
+    }
+
+    public long getTimestamp() {
+        return mTimestamp;
+    }
+
+    public String getAction() {
+        return mAction;
+    }
+
+    public String getActionUri() {
+        return mActionUri;
+    }
+
+    public String getResPackage() {
+        return mResPackage;
+    }
+
+    public int getIconRes() {
+        return mIconRes;
+    }
+
+    public int getLabelRes() {
+        return mLabelRes;
+    }
+
+    public List<StreamItemPhotoEntry> getPhotos() {
+        Collections.sort(mPhotos);
+        return mPhotos;
+    }
+
+    private static String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName, int missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+    }
+
+    private static long getLong(Cursor cursor, String columnName) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.getLong(columnIndex);
+    }
+}
diff --git a/src/com/android/contacts/util/StreamItemPhotoEntry.java b/src/com/android/contacts/util/StreamItemPhotoEntry.java
new file mode 100644
index 0000000..6527454
--- /dev/null
+++ b/src/com/android/contacts/util/StreamItemPhotoEntry.java
@@ -0,0 +1,139 @@
+/*
+ * 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 android.database.Cursor;
+import android.provider.ContactsContract.PhotoFiles;
+import android.provider.ContactsContract.StreamItemPhotos;
+
+/**
+ * Data object for a photo associated with a social stream item.  These are comparable;
+ * entries with a lower sort index will be displayed on top (with the ID used as a
+ * tie-breaker).
+ */
+public class StreamItemPhotoEntry implements Comparable<StreamItemPhotoEntry> {
+    private final long mId;
+    private final int mSortIndex;
+    private final long mPhotoFileId;
+    private final String mPhotoUri;
+    private final int mHeight;
+    private final int mWidth;
+    private final int mFileSize;
+    private final String mAction;
+    private final String mActionUri;
+
+    public StreamItemPhotoEntry(long id, int sortIndex, long photoFileId, String photoUri,
+            int height, int width, int fileSize, String action, String actionUri) {
+        mId = id;
+        mSortIndex = sortIndex;
+        mPhotoFileId = photoFileId;
+        mPhotoUri = photoUri;
+        mHeight = height;
+        mWidth = width;
+        mFileSize = fileSize;
+        mAction = action;
+        mActionUri = actionUri;
+    }
+
+    public StreamItemPhotoEntry(Cursor cursor) {
+        // This is expected to be populated via a cursor containing a join of all
+        // StreamItemPhotos columns and all PhotoFiles columns (except for ID).
+        mId = getLong(cursor, StreamItemPhotos._ID);
+        mSortIndex = getInt(cursor, StreamItemPhotos.SORT_INDEX, -1);
+        mPhotoFileId = getLong(cursor, StreamItemPhotos.PHOTO_FILE_ID);
+        mPhotoUri = getString(cursor, StreamItemPhotos.PHOTO_URI);
+        mHeight = getInt(cursor, PhotoFiles.HEIGHT, -1);
+        mWidth = getInt(cursor, PhotoFiles.WIDTH, -1);
+        mFileSize = getInt(cursor, PhotoFiles.FILESIZE, -1);
+        mAction = getString(cursor, StreamItemPhotos.ACTION);
+        mActionUri = getString(cursor, StreamItemPhotos.ACTION_URI);
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    public int getSortIndex() {
+        return mSortIndex;
+    }
+
+    public long getPhotoFileId() {
+        return mPhotoFileId;
+    }
+
+    public String getPhotoUri() {
+        return mPhotoUri;
+    }
+
+    public int getHeight() {
+        return mHeight;
+    }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public int getFileSize() {
+        return mFileSize;
+    }
+
+    public String getAction() {
+        return mAction;
+    }
+
+    public String getActionUri() {
+        return mActionUri;
+    }
+
+    @Override
+    public int compareTo(StreamItemPhotoEntry streamItemPhotoEntry) {
+        // Sort index is used to compare, falling back to ID if neither entry has a
+        // sort index specified (entries without a sort index are sorted after entries
+        // that have one).
+        if (mSortIndex == streamItemPhotoEntry.mSortIndex) {
+            if (mSortIndex == -1) {
+                return mId == streamItemPhotoEntry.mId ? 0
+                        : mId < streamItemPhotoEntry.mId ? -1 : 1;
+            } else {
+                return 0;
+            }
+        } else {
+            if (mSortIndex == -1) {
+                return 1;
+            }
+            if (streamItemPhotoEntry.mSortIndex == -1) {
+                return -1;
+            }
+            return mSortIndex == streamItemPhotoEntry.mSortIndex ? 0
+                    : mSortIndex < streamItemPhotoEntry.mSortIndex ? -1 : 1;
+        }
+    }
+
+    private static String getString(Cursor cursor, String columnName) {
+        return cursor.getString(cursor.getColumnIndex(columnName));
+    }
+
+    private static int getInt(Cursor cursor, String columnName, int missingValue) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.isNull(columnIndex) ? missingValue : cursor.getInt(columnIndex);
+    }
+
+    private static long getLong(Cursor cursor, String columnName) {
+        final int columnIndex = cursor.getColumnIndex(columnName);
+        return cursor.getLong(columnIndex);
+    }
+}