Merge "Auto select first group in group list on tablet"
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/src/com/android/contacts/detail/ContactDetailDisplayUtils.java b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
index d8ba995..a22102a 100644
--- a/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
+++ b/src/com/android/contacts/detail/ContactDetailDisplayUtils.java
@@ -251,24 +251,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/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);