Merge "Move notification generation to a service."
diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml
index 8677737..95eb0a5 100644
--- a/res/layout/contact_detail_updates_fragment.xml
+++ b/res/layout/contact_detail_updates_fragment.xml
@@ -54,9 +54,7 @@
                 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" />
+                android:layout_height="wrap_content" />
         </LinearLayout>
 
     </ScrollView>
diff --git a/res/layout/stream_item_one_column.xml b/res/layout/stream_item_one_column.xml
index 014e3f1..ecab57c 100644
--- a/res/layout/stream_item_one_column.xml
+++ b/res/layout/stream_item_one_column.xml
@@ -17,20 +17,24 @@
 <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:paddingTop="@dimen/detail_update_section_item_vertical_padding"
         android:paddingBottom="@dimen/detail_update_section_item_vertical_padding"
-        android:paddingLeft="@dimen/detail_update_section_item_left_padding"
+        android:paddingLeft="@dimen/detail_update_section_item_horizontal_padding"
+        android:paddingRight="@dimen/detail_update_section_item_horizontal_padding"
+        android:background="@drawable/list_selector"
         android:orientation="vertical" />
 
     <View
         android:id="@+id/horizontal_divider"
         android:layout_width="match_parent"
         android:layout_height="1px"
+        android:layout_marginLeft="@dimen/detail_update_section_side_padding"
+        android:layout_marginRight="@dimen/detail_update_section_side_padding"
         android:background="?android:attr/dividerHorizontal" />
 </LinearLayout>
diff --git a/res/layout/stream_item_text.xml b/res/layout/stream_item_text.xml
index 4c44100..861d91f 100644
--- a/res/layout/stream_item_text.xml
+++ b/res/layout/stream_item_text.xml
@@ -43,4 +43,4 @@
             android:textColor="@color/social_update_comments_color" />
     </LinearLayout>
 
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 604ea31..69c89d8 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -109,8 +109,8 @@
     <!-- 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 for individual stream items -->
+    <dimen name="detail_update_section_item_horizontal_padding">24dip</dimen>
 
     <!-- Horizontal padding between content sections within a stream item -->
     <dimen name="detail_update_section_internal_padding">16dip</dimen>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 999531e..b1f9f6f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1019,8 +1019,8 @@
          was found that could perform the selected action -->
     <string name="quickcontact_missing_app">No application found to handle this action</string>
 
-    <!-- Shown as the header name for a person when the name is missing or unknown. -->
-    <string name="quickcontact_missing_name">Unknown</string>
+    <!-- Shown as the display name for a person when the name is missing or unknown. [CHAR LIMIT=18]-->
+    <string name="missing_name">(no name)</string>
 
     <!-- The menu item to open the list of accounts -->
     <string name="menu_accounts">Accounts</string>
diff --git a/src/com/android/contacts/activities/DialpadActivity.java b/src/com/android/contacts/activities/DialpadActivity.java
index cfe17f3..1221068 100644
--- a/src/com/android/contacts/activities/DialpadActivity.java
+++ b/src/com/android/contacts/activities/DialpadActivity.java
@@ -64,7 +64,7 @@
     @Override
     protected void onNewIntent(Intent newIntent) {
         setIntent(newIntent);
-        mFragment.resolveIntent(newIntent);
+        mFragment.configureScreenFromIntent(newIntent);
     }
 
     public DialpadFragment getFragment() {
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 01212f5..2040f8d 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -481,12 +481,18 @@
         final String action = newIntent.getAction();
         if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
             setupFilterText(newIntent);
-        } else if (isDialIntent(newIntent)) {
-            setupDialUri(newIntent);
         }
         if (mInSearchUi || mSearchFragment.isVisible()) {
             exitSearchUi();
         }
+
+        if (mViewPager.getCurrentItem() == TAB_INDEX_DIALER) {
+            if (mDialpadFragment != null) {
+                mDialpadFragment.configureScreenFromIntent(newIntent);
+            } else {
+                Log.e(TAG, "DialpadFragment isn't ready yet when the tab is already selected.");
+            }
+        }
     }
 
     /** Returns true if the given intent contains a phone number to populate the dialer with */
@@ -535,33 +541,6 @@
         }
     }
 
-    /**
-     * Retrieves the uri stored in {@link #setupDialUri(Intent)}. This uri
-     * originally came from a dial intent received by this activity. The stored
-     * uri will then be cleared after after this method returns.
-     *
-     * @return The stored uri
-     */
-    public Uri getAndClearDialUri() {
-        Uri dialUri = mDialUri;
-        mDialUri = null;
-        return dialUri;
-    }
-
-    /**
-     * Stores the uri associated with a dial intent. This is so child activities can
-     * check if they are supposed to display new dial info.
-     *
-     * @param intent The intent received in {@link #onNewIntent(Intent)}
-     */
-    private void setupDialUri(Intent intent) {
-        // If the intent was relaunched from history, don't reapply the intent.
-        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
-            return;
-        }
-        mDialUri = intent.getData();
-    }
-
     @Override
     public void onBackPressed() {
         if (mInSearchUi) {
diff --git a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index d8ba995..7144dfb 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -99,6 +99,8 @@
                     styledName = altDisplayName;
                 }
             }
+        } else {
+            styledName = context.getResources().getString(R.string.missing_name);
         }
         return styledName;
     }
@@ -251,24 +253,33 @@
      * Displays the social stream items under the given layout.
      */
     public static void showSocialStreamItems(LayoutInflater inflater, Context context,
-            Result contactData, LinearLayout streamContainer) {
+            Result contactData, LinearLayout streamContainer, View.OnClickListener listener) {
         if (streamContainer != null) {
             streamContainer.removeAllViews();
             List<StreamItemEntry> streamItems = contactData.getStreamItems();
             for (StreamItemEntry streamItem : streamItems) {
-                addStreamItemToContainer(inflater, context, streamItem, streamContainer);
+                addStreamItemToContainer(inflater, context, streamItem, streamContainer, listener);
             }
         }
     }
 
-    public static void addStreamItemToContainer(LayoutInflater inflater, Context context,
-            StreamItemEntry streamItem, LinearLayout streamContainer) {
+    @VisibleForTesting
+    static void addStreamItemToContainer(LayoutInflater inflater, Context context,
+            StreamItemEntry streamItem, LinearLayout streamContainer,
+            View.OnClickListener listener) {
         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);
 
+        // Add the listener only if there is an action and corresponding URI.
+        if (streamItem.getAction() != null && streamItem.getActionUri() != null) {
+            contentBox.setTag(streamItem);
+            contentBox.setOnClickListener(listener);
+            contentBox.setFocusable(true);
+        }
+
         // 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
diff --git a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
index 602958d..d668429 100644
--- a/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
+++ b/src/com/android/contacts/detail/ContactDetailUpdatesFragment.java
@@ -19,10 +19,13 @@
 import com.android.contacts.ContactLoader;
 import com.android.contacts.R;
 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
+import com.android.contacts.util.StreamItemEntry;
 
 import android.app.Fragment;
+import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -54,6 +57,28 @@
      */
     private View mTouchInterceptLayer;
 
+    /**
+     * Listener on clicks on a stream item.
+     * <p>
+     * It assumes the view has a tag of type {@link StreamItemEntry} associated with it.
+     */
+    private View.OnClickListener mStreamItemClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            StreamItemEntry streamItemEntry = (StreamItemEntry) view.getTag();
+            Uri uri;
+            try {
+                uri = Uri.parse(streamItemEntry.getActionUri());
+            } catch (Throwable throwable) {
+                Log.e(TAG, "invalid URI for stream item #" + streamItemEntry.getId() + ": "
+                        + streamItemEntry.getActionUri());
+                return;
+            }
+            Intent streamItemIntent = new Intent(streamItemEntry.getAction(), uri);
+            startActivity(streamItemIntent);
+        }
+    };
+
     public ContactDetailUpdatesFragment() {
         // Explicit constructor for inflation
     }
@@ -75,7 +100,7 @@
         // have it.
         if (mContactData != null) {
             ContactDetailDisplayUtils.showSocialStreamItems(inflater, getActivity(), mContactData,
-                    mStreamContainer);
+                    mStreamContainer, mStreamItemClickListener);
         }
 
         mAlphaLayer = rootView.findViewById(R.id.alpha_overlay);
@@ -91,7 +116,7 @@
         mLookupUri = lookupUri;
         mContactData = result;
         ContactDetailDisplayUtils.showSocialStreamItems(mInflater, getActivity(), mContactData,
-                mStreamContainer);
+                mStreamContainer, mStreamItemClickListener);
     }
 
     @Override
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index add6b80..a5db5ce 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -143,9 +143,6 @@
     static final String EXTRA_SEND_EMPTY_FLASH
             = "com.android.phone.extra.SEND_EMPTY_FLASH";
 
-    /** Indicates if we are opening this dialer to add a call from the InCallScreen. */
-    private boolean mIsAddCallMode;
-
     private String mCurrentCountryIso;
 
     /**
@@ -313,7 +310,7 @@
         mDialpadChooser = (ListView) fragmentView.findViewById(R.id.dialpadChooser);
         mDialpadChooser.setOnItemClickListener(this);
 
-        resolveIntent(getActivity().getIntent());
+        configureScreenFromIntent(getActivity().getIntent());
 
         return fragmentView;
     }
@@ -323,38 +320,18 @@
     }
 
     /**
-     * Handles the intent that launched us.
-     *
-     * We can be launched either with ACTION_DIAL or ACTION_VIEW (which
-     * may include a phone number to pre-load), or ACTION_MAIN (which just
-     * brings up a blank dialpad).
-     *
-     * @return true IFF the current intent has the DialtactsActivity.EXTRA_IGNORE_STATE
-     *    extra set to true, which indicates (to our container) that we should ignore
-     *    any possible saved state, and instead reset our state based on the parent's
-     *    intent.
+     * @return true when {@link #mDigits} is actually filled by the Intent.
      */
-    public boolean resolveIntent(Intent intent) {
-        boolean ignoreState = false;
-
-        // by default we are not adding a call.
-        mIsAddCallMode = false;
-
-        // By default we don't show the "dialpad chooser" UI.
-        boolean needToShowDialpadChooser = false;
-
-        // Resolve the intent
+    private boolean fillDigitsIfNecessary(Intent intent) {
         final String action = intent.getAction();
         if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
-            // see if we are "adding a call" from the InCallScreen; false by default.
-            mIsAddCallMode = intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
-
             Uri uri = intent.getData();
             if (uri != null) {
                 if ("tel".equals(uri.getScheme())) {
                     // Put the requested number into the input area
                     String data = uri.getSchemeSpecificPart();
                     setFormattedDigits(data, null);
+                    return true;
                 } else {
                     String type = intent.getType();
                     if (People.CONTENT_ITEM_TYPE.equals(type)
@@ -364,22 +341,42 @@
                                 new String[] {PhonesColumns.NUMBER, PhonesColumns.NUMBER_KEY},
                                 null, null, null);
                         if (c != null) {
-                            if (c.moveToFirst()) {
-                                // Put the number into the input area
-                                setFormattedDigits(c.getString(0), c.getString(1));
+                            try {
+                                if (c.moveToFirst()) {
+                                    // Put the number into the input area
+                                    setFormattedDigits(c.getString(0), c.getString(1));
+                                    return true;
+                                }
+                            } finally {
+                                c.close();
                             }
-                            c.close();
                         }
                     }
                 }
-            } else {
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @see #showDialpadChooser(boolean)
+     */
+    private static boolean needToShowDialpadChooser(Intent intent, boolean isAddCallMode) {
+        final String action = intent.getAction();
+
+        boolean needToShowDialpadChooser = false;
+
+        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+            Uri uri = intent.getData();
+            if (uri == null) {
                 // ACTION_DIAL or ACTION_VIEW with no data.
                 // This behaves basically like ACTION_MAIN: If there's
                 // already an active call, bring up an intermediate UI to
                 // make the user confirm what they really want to do.
                 // Be sure *not* to show the dialpad chooser if this is an
                 // explicit "Add call" action, though.
-                if (!mIsAddCallMode && phoneIsInUse()) {
+                if (!isAddCallMode && phoneIsInUse()) {
                     needToShowDialpadChooser = true;
                 }
             }
@@ -399,11 +396,34 @@
             }
         }
 
-        // Bring up the "dialpad chooser" IFF we need to make the user
-        // confirm which dialpad they really want.
-        showDialpadChooser(needToShowDialpadChooser);
+        return needToShowDialpadChooser;
+    }
 
-        return ignoreState;
+    private static boolean isAddCallMode(Intent intent) {
+        final String action = intent.getAction();
+        if (Intent.ACTION_DIAL.equals(action) || Intent.ACTION_VIEW.equals(action)) {
+            // see if we are "adding a call" from the InCallScreen; false by default.
+            return intent.getBooleanExtra(ADD_CALL_MODE_KEY, false);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Checks the given Intent and changes dialpad's UI state. For example, if the Intent requires
+     * the screen to enter "Add Call" mode, this method will show correct UI for the mode.
+     */
+    public void configureScreenFromIntent(Intent intent) {
+        boolean needToShowDialpadChooser = false;
+
+        final boolean isAddCallMode = isAddCallMode(intent);
+        if (!isAddCallMode) {
+            final boolean digitsFilled = fillDigitsIfNecessary(intent);
+            if (!digitsFilled) {
+                needToShowDialpadChooser = needToShowDialpadChooser(intent, isAddCallMode);
+            }
+        }
+        showDialpadChooser(needToShowDialpadChooser);
     }
 
     private void setFormattedDigits(String data, String normalizedNumber) {
@@ -476,13 +496,10 @@
         }
 
         Activity parent = getActivity();
-        // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
-        // digits in the dialer field.
         if (parent instanceof DialtactsActivity) {
-            Uri dialUri = ((DialtactsActivity) parent).getAndClearDialUri();
-            if (dialUri != null) {
-                resolveIntent(parent.getIntent());
-            }
+            // See if we were invoked with a DIAL intent. If we were, fill in the appropriate
+            // digits in the dialer field.
+            fillDigitsIfNecessary(parent.getIntent());
         }
 
         // While we're in the foreground, listen for phone state changes,
@@ -904,8 +921,8 @@
             // ListView.  We do this only once.
             if (mDialpadChooserAdapter == null) {
                 mDialpadChooserAdapter = new DialpadChooserAdapter(getActivity());
-                mDialpadChooser.setAdapter(mDialpadChooserAdapter);
             }
+            mDialpadChooser.setAdapter(mDialpadChooserAdapter);
         } else {
             // Log.i(TAG, "Displaying normal Dialer UI.");
             mDigits.setVisibility(View.VISIBLE);
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index 753261a..630a397 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -56,6 +56,15 @@
 
     public void setCursor(Cursor cursor) {
         mCursor = cursor;
+
+        // If there's no selected group already and the cursor is valid, then by default, select the
+        // first group
+        if (mSelectedGroupUri == null && cursor != null && cursor.getCount() > 0) {
+            GroupListItem firstItem = getItem(0);
+            long groupId = (firstItem == null) ? null : firstItem.getGroupId();
+            mSelectedGroupUri = getGroupUriFromId(groupId);
+        }
+
         notifyDataSetChanged();
     }
 
@@ -89,6 +98,10 @@
         return mSelectedGroupUri != null && mSelectedGroupUri.equals(groupUri);
     }
 
+    public Uri getSelectedGroup() {
+        return mSelectedGroupUri;
+    }
+
     @Override
     public int getCount() {
         return mCursor == null ? 0 : mCursor.getCount();
diff --git a/src/com/android/contacts/group/GroupBrowseListFragment.java b/src/com/android/contacts/group/GroupBrowseListFragment.java
index a1544cf..835400f 100644
--- a/src/com/android/contacts/group/GroupBrowseListFragment.java
+++ b/src/com/android/contacts/group/GroupBrowseListFragment.java
@@ -223,6 +223,7 @@
         }
         mListView.setEmptyView(mEmptyView);
 
+        mSelectedGroupUri = mAdapter.getSelectedGroup();
         if (mSelectionVisible && mSelectedGroupUri != null) {
             viewGroup(mSelectedGroupUri);
         }
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index 7322fc6..467dcc3 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -15,6 +15,8 @@
  */
 package com.android.contacts.list;
 
+import com.android.contacts.R;
+
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
@@ -28,7 +30,6 @@
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ListView;
-import android.widget.QuickContactBadge;
 
 /**
  * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
@@ -121,7 +122,7 @@
     public ContactListAdapter(Context context) {
         super(context);
 
-        mUnknownNameText = context.getText(android.R.string.unknownName);
+        mUnknownNameText = context.getText(R.string.missing_name);
         mViewTypeProfileEntry = getViewTypeCount() - 1;
     }
 
diff --git a/src/com/android/contacts/list/ContactTileAdapter.java b/src/com/android/contacts/list/ContactTileAdapter.java
index 8993cdd..1e65612 100644
--- a/src/com/android/contacts/list/ContactTileAdapter.java
+++ b/src/com/android/contacts/list/ContactTileAdapter.java
@@ -24,6 +24,7 @@
 
 import android.content.ContentUris;
 import android.content.Context;
+import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.provider.ContactsContract;
@@ -50,6 +51,7 @@
     private DisplayType mDisplayType;
     private Listener mListener;
     private Context mContext;
+    private Resources mResources;
     private Cursor mContactCursor = null;
     private ContactPhotoManager mPhotoManager;
 
@@ -114,6 +116,7 @@
             DisplayType displayType) {
         mListener = listener;
         mContext = context;
+        mResources = context.getResources();
         mColumnCount = (displayType == DisplayType.FREQUENT_ONLY ? 1 : numCols);
         mDisplayType = displayType;
 
@@ -213,7 +216,8 @@
         String lookupKey = cursor.getString(mLookupIndex);
 
         ContactEntry contact = new ContactEntry();
-        contact.name = cursor.getString(mNameIndex);
+        String name = cursor.getString(mNameIndex);
+        contact.name = (name != null) ? name : mResources.getString(R.string.missing_name);
         contact.status = cursor.getString(mStatusIndex);
         contact.photoUri = (photoUri != null ? Uri.parse(photoUri) : null);
         contact.lookupKey = ContentUris.withAppendedId(
@@ -223,8 +227,8 @@
         if (mDisplayType == DisplayType.STREQUENT_PHONE_ONLY) {
             int phoneNumberType = cursor.getInt(mPhoneNumberTypeIndex);
             String phoneNumberCustomLabel = cursor.getString(mPhoneNumberLabelIndex);
-            contact.phoneLabel = (String) Phone.getTypeLabel(mContext.getResources(),
-                    phoneNumberType, phoneNumberCustomLabel);
+            contact.phoneLabel = (String) Phone.getTypeLabel(mResources, phoneNumberType,
+                    phoneNumberCustomLabel);
             contact.phoneNumber = cursor.getString(mPhoneNumberIndex);
         } else {
             contact.status = cursor.getString(mStatusIndex);
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 47e7173..d236e01 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -228,7 +228,7 @@
 
         // find and prepare correct header view
         mPhotoContainer = findViewById(R.id.photo_container);
-        setHeaderText(R.id.name, R.string.quickcontact_missing_name);
+        setHeaderNameText(R.id.name, R.string.missing_name);
         setHeaderText(R.id.status, null);
         setHeaderText(R.id.timestamp, null);
         setHeaderImage(R.id.presence, null);
@@ -317,12 +317,33 @@
         }
     };
 
-    /** Assign this string to the view, if found in {@link #mPhotoContainer}. */
+    /** Assign this string to the view if it is not empty. */
+    private void setHeaderNameText(int id, int resId) {
+        setHeaderNameText(id, getText(resId));
+    }
+
+    /** Assign this string to the view if it is not empty. */
+    private void setHeaderNameText(int id, CharSequence value) {
+        final View view = mPhotoContainer.findViewById(id);
+        if (view instanceof TextView) {
+            if (!TextUtils.isEmpty(value)) {
+                ((TextView)view).setText(value);
+            }
+        }
+    }
+
+    /**
+     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
+     * if there is no string.
+     */
     private void setHeaderText(int id, int resId) {
         setHeaderText(id, getText(resId));
     }
 
-    /** Assign this string to the view, if found in {@link #mPhotoContainer}. */
+    /**
+     * Assign this string to the view (if found in {@link #mPhotoContainer}), or hiding this view
+     * if there is no string.
+     */
     private void setHeaderText(int id, CharSequence value) {
         final View view = mPhotoContainer.findViewById(id);
         if (view instanceof TextView) {
@@ -483,7 +504,7 @@
             final Drawable statusIcon = ContactPresenceIconUtil.getChatCapabilityIcon(
                     context, presence, chatCapability);
 
-            setHeaderText(R.id.name, name);
+            setHeaderNameText(R.id.name, name);
             // TODO: Bring this back once we have a design
 //            setHeaderImage(R.id.presence, statusIcon);
         }
diff --git a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
index 3d7881b..4686c81 100644
--- a/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
+++ b/src/com/android/contacts/socialwidget/SocialWidgetProvider.java
@@ -186,6 +186,10 @@
         SpannableStringBuilder sb = new SpannableStringBuilder();
 
         CharSequence name = displayName;
+        // If there is no display name, use the default missing name string
+        if (TextUtils.isEmpty(name)) {
+            name = context.getString(R.string.missing_name);
+        }
         if (!TextUtils.isEmpty(phoneticName)) {
             name = context.getString(R.string.widget_name_and_phonetic,
                     name, phoneticName);
diff --git a/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java b/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
index f9b33e0..98001ae 100644
--- a/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
+++ b/tests/src/com/android/contacts/detail/ContactDetailDisplayUtilsTest.java
@@ -37,8 +37,9 @@
 public class ContactDetailDisplayUtilsTest extends AndroidTestCase {
     private static final String TEST_STREAM_ITEM_TEXT = "text";
 
-    private ViewGroup mParent;
+    private LinearLayout mParent;
     private LayoutInflater mLayoutInflater;
+    private FakeOnClickListener mListener = new FakeOnClickListener();
 
     @Override
     protected void setUp() throws Exception {
@@ -71,13 +72,79 @@
         assertGone(streamItemView, R.id.stream_item_comments);
     }
 
-    /**
-     * Calls {@link ContactDetailDisplayUtils#addStreamItemText(LayoutInflater, Context,
-     * StreamItemEntry, ViewGroup)} with the default parameters and the given stream item.
-     */
-    private View addStreamItemText(StreamItemEntry streamItem) {
-        return ContactDetailDisplayUtils.addStreamItemText(
-                mLayoutInflater, getContext(), streamItem, mParent);
+    public void testAddStreamItemToContainer_NoAction() {
+        StreamItemEntry streamItem = getTestBuilder()
+                .setAction(null)
+                .setActionUri(null)
+                .build();
+        addStreamItemToContainer(streamItem, mListener);
+        assertStreamItemNotClickable();
+    }
+
+    public void testAddStreamItemToContainer_WithActionButNoActionUri() {
+        StreamItemEntry streamItem = getTestBuilder()
+                .setAction("action")
+                .setActionUri(null)
+                .build();
+        addStreamItemToContainer(streamItem, mListener);
+        assertStreamItemNotClickable();
+    }
+
+    public void testAddStreamItemToContainer_WithActionUriButNoAction() {
+        StreamItemEntry streamItem = getTestBuilder()
+                .setAction(null)
+                .setActionUri("http://www.google.com")
+                .build();
+        addStreamItemToContainer(streamItem, mListener);
+        assertStreamItemNotClickable();
+    }
+
+    public void testAddStreamItemToContainer_WithActionAndActionUri() {
+        StreamItemEntry streamItem = getTestBuilder()
+                .setAction("action")
+                .setActionUri("http://www.google.com")
+                .build();
+        addStreamItemToContainer(streamItem, mListener);
+        assertStreamItemClickable();
+        assertStreamItemHasOnClickListener();
+        assertStreamItemHasTag(streamItem);
+    }
+
+    /** Checks that the stream item view is clickable. */
+    private void assertStreamItemClickable() {
+        View streamItemView = mParent.findViewById(R.id.stream_item_content);
+        assertNotNull("should have a stream item", streamItemView);
+        assertTrue("should be clickable", streamItemView.isClickable());
+        assertTrue("should be focusable", streamItemView.isFocusable());
+    }
+
+    /** Asserts that there is a stream item but it is not clickable. */
+    private void assertStreamItemNotClickable() {
+        View streamItemView = mParent.findViewById(R.id.stream_item_content);
+        assertNotNull("should have a stream item", streamItemView);
+        assertFalse("should not be clickable", streamItemView.isClickable());
+        assertFalse("should not be focusable", streamItemView.isFocusable());
+    }
+
+    /** Checks that the stream item view has a click listener. */
+    private void assertStreamItemHasOnClickListener() {
+        // Check that the on-click listener is invoked when clicked.
+        View streamItemView = mParent.findViewById(R.id.stream_item_content);
+        assertFalse("listener should have not been invoked yet", mListener.clicked);
+        streamItemView.performClick();
+        assertTrue("listener should have been invoked", mListener.clicked);
+    }
+
+    /** Checks that the stream item view has the given stream item as its tag. */
+    private void assertStreamItemHasTag(StreamItemEntry streamItem) {
+        // The view's tag should point to the stream item entry for this view.
+        View streamItemView = mParent.findViewById(R.id.stream_item_content);
+        Object tag = streamItemView.getTag();
+        assertNotNull("should have a tag", tag);
+        assertTrue("should be a StreamItemEntry", tag instanceof StreamItemEntry);
+        StreamItemEntry streamItemTag = (StreamItemEntry) tag;
+        // The streamItem itself should be in the tag.
+        assertSame(streamItem, streamItemTag);
     }
 
     /** Checks that the given id corresponds to a visible text view with the expected text. */
@@ -101,7 +168,8 @@
      */
     private void assertSpannableEquals(Spanned expected, CharSequence actualCharSequence) {
         assertEquals(expected.toString(), actualCharSequence.toString());
-        assertTrue(actualCharSequence instanceof Spanned);
+        assertTrue("char sequence should be an instance of Spanned",
+                actualCharSequence instanceof Spanned);
         Spanned actual = (Spanned) actualCharSequence;
         assertEquals(Html.toHtml(expected), Html.toHtml(actual));
     }
@@ -113,6 +181,39 @@
         assertEquals(View.GONE, view.getVisibility());
     }
 
+    /**
+     * Calls {@link ContactDetailDisplayUtils#addStreamItemText(LayoutInflater, Context,
+     * StreamItemEntry, ViewGroup)} with the default parameters and the given stream item.
+     */
+    private View addStreamItemText(StreamItemEntry streamItem) {
+        return ContactDetailDisplayUtils.addStreamItemText(
+                mLayoutInflater, getContext(), streamItem, mParent);
+    }
+
+    /**
+     * Calls {@link ContactDetailDisplayUtils#addStreamItemToContainer(LayoutInflater,
+     * Context,StreamItemEntry, LinearLayout, android.view.View.OnClickListener)} with the default
+     * parameters and the given stream item and listener.
+     */
+    private void addStreamItemToContainer(StreamItemEntry streamItem,
+            View.OnClickListener listener) {
+        ContactDetailDisplayUtils.addStreamItemToContainer(mLayoutInflater, getContext(),
+                streamItem, mParent, listener);
+    }
+
+    /**
+     * Simple fake implementation of {@link View.OnClickListener} which sets a member variable to
+     * true when clicked.
+     */
+    private final class FakeOnClickListener implements View.OnClickListener {
+        public boolean clicked = false;
+
+        @Override
+        public void onClick(View view) {
+            clicked = true;
+        }
+    }
+
     private static class StreamItemEntryBuilder {
         private long mId;
         private String mText;
@@ -136,6 +237,16 @@
             return this;
         }
 
+        public StreamItemEntryBuilder setAction(String action) {
+            mAction = action;
+            return this;
+        }
+
+        public StreamItemEntryBuilder setActionUri(String actionUri) {
+            mActionUri = actionUri;
+            return this;
+        }
+
         public StreamItemEntry build() {
             return new StreamItemEntry(mId, mText, mComment, mTimestamp, mAction, mActionUri,
                     mResPackage, mIconRes, mLabelRes);