Add actions to update items.

This commit let you click on an update item if it has an associated
action and actionUri.

In order to make this visible to the user, it adds a background that
will be highlighted when the item is selected. It also marks the item as
focusable for accessibility.

In order to make the selection expand to the entire row, play around
with the padding a little bit: basically, the update list itself is
now as wide as the screen, and the individual items have padding as
appropriate.

Bug: 5095755
Change-Id: Ib1b2d179152beae125dded1b393b3dfc8b22abc9
diff --git a/res/layout/contact_detail_updates_fragment.xml b/res/layout/contact_detail_updates_fragment.xml
index 92f3575..f50630e 100644
--- a/res/layout/contact_detail_updates_fragment.xml
+++ b/res/layout/contact_detail_updates_fragment.xml
@@ -39,9 +39,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 45aa75e..28a18cd 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -120,8 +120,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);