Dedupe same entries in the fasttrack action list.

Fixes b/2159633.
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 1e3b8ad..622edbe 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -37,6 +37,7 @@
 import android.provider.ContactsContract.CommonDataKinds.Photo;
 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
 import android.provider.Im.ProviderNames;
+import android.telephony.PhoneNumberUtils;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -424,4 +425,47 @@
     public static boolean isGraphic(CharSequence str) {
         return !TextUtils.isEmpty(str) && TextUtils.isGraphic(str);
     }
+
+    /**
+     * Returns true if two objects are considered equal.  Two null references are equal here.
+     */
+    public static boolean areObjectsEqual(Object a, Object b) {
+        return a == b || (a != null && a.equals(b));
+    }
+
+    /**
+     * Returns true if two data with mimetypes which represent values in contact entries are
+     * considered equal.
+     */
+    public static final boolean areDataEqual(Context context, CharSequence mimetype1,
+            CharSequence data1, CharSequence mimetype2, CharSequence data2) {
+        if (TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype1)
+                && TextUtils.equals(Phone.CONTENT_ITEM_TYPE, mimetype2)) {
+            if (data1 == data2) {
+                return true;
+            }
+            if (data1 == null || data2 == null) {
+                return false;
+            }
+            return PhoneNumberUtils.compare(context, data1.toString(), data2.toString());
+        } else {
+            if (mimetype1 == mimetype2 && data1 == data2) {
+                return true;
+            }
+            return TextUtils.equals(mimetype1, mimetype2) && TextUtils.equals(data1, data2);
+        }
+    }
+
+    /**
+     * Returns true if two {@link Intent}s are both null, or have the same action.
+     */
+    public static final boolean areIntentActionEqual(Intent a, Intent b) {
+        if (a == b) {
+            return true;
+        }
+        if (a == null || b == null) {
+            return false;
+        }
+        return TextUtils.equals(a.getAction(), b.getAction());
+    }
 }
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index 06296cc..a1aeca3 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -1026,39 +1026,19 @@
                 return false;
             }
 
-            if (Phone.CONTENT_ITEM_TYPE.equals(mimetype)
-                    && Phone.CONTENT_ITEM_TYPE.equals(entry.mimetype)) {
-                if (!PhoneNumberUtils.compare(this.context, data, entry.data)) {
-                    return false;
-                }
-            } else {
-                if (!equals(data, entry.data)) {
-                    return false;
-                }
+            if (!ContactsUtils.areDataEqual(context, mimetype, data, entry.mimetype, entry.data)) {
+                return false;
             }
 
-            if (!equals(mimetype, entry.mimetype)
-                    || !intentCollapsible(intent, entry.intent)
-                    || !intentCollapsible(secondaryIntent, entry.secondaryIntent)
+            if (!TextUtils.equals(mimetype, entry.mimetype)
+                    || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
+                    || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
                     || actionIcon != entry.actionIcon) {
                 return false;
             }
 
             return true;
         }
-
-        private boolean equals(Object a, Object b) {
-            return a==b || (a != null && a.equals(b));
-        }
-
-        private boolean intentCollapsible(Intent a, Intent b) {
-            if (a == b) {
-                return true;
-            } else if ((a != null && b != null) && equals(a.getAction(), b.getAction())) {
-                return true;
-            }
-            return false;
-        }
     }
 
     /** Cache of the children views of a row */
diff --git a/src/com/android/contacts/ui/QuickContactWindow.java b/src/com/android/contacts/ui/QuickContactWindow.java
index e787987..f43c17e 100644
--- a/src/com/android/contacts/ui/QuickContactWindow.java
+++ b/src/com/android/contacts/ui/QuickContactWindow.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.ui;
 
+import com.android.contacts.Collapser;
 import com.android.contacts.ContactsUtils;
 import com.android.contacts.R;
 import com.android.contacts.model.ContactsSource;
@@ -87,6 +88,7 @@
 import android.widget.Toast;
 
 import java.lang.ref.SoftReference;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -658,7 +660,7 @@
      * Abstract definition of an action that could be performed, along with
      * string description and icon.
      */
-    private interface Action {
+    private interface Action extends Collapser.Collapsible<Action> {
         public CharSequence getHeader();
         public CharSequence getBody();
 
@@ -844,6 +846,39 @@
         public Intent getIntent() {
             return mIntent;
         }
+
+        /** {@inheritDoc} */
+        public boolean collapseWith(Action other) {
+            if (!shouldCollapseWith(other)) {
+                return false;
+            }
+            return true;
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldCollapseWith(Action t) {
+            if (t == null) {
+                return false;
+            }
+            if (!(t instanceof DataAction)) {
+                Log.e(TAG, "t must be DataAction");
+                return false;
+            }
+            DataAction other = (DataAction)t;
+            if (!ContactsUtils.areObjectsEqual(mKind, other.mKind)) {
+                return false;
+            }
+            if (!ContactsUtils.areDataEqual(mContext, mMimeType, mBody, other.mMimeType,
+                    other.mBody)) {
+                return false;
+            }
+            if (!TextUtils.equals(mMimeType, other.mMimeType)
+                    || !ContactsUtils.areIntentActionEqual(mIntent, other.mIntent)
+                    ) {
+                return false;
+            }
+            return true;
+        }
     }
 
     /**
@@ -895,6 +930,15 @@
             return null;
         }
 
+        /** {@inheritDoc} */
+        public boolean collapseWith(Action t) {
+            return false; // Never dup.
+        }
+
+        /** {@inheritDoc} */
+        public boolean shouldCollapseWith(Action t) {
+            return false; // Never dup.
+        }
     }
 
     /**
@@ -1039,7 +1083,7 @@
      * Provide a strongly-typed {@link LinkedList} that holds a list of
      * {@link Action} objects.
      */
-    private class ActionList extends LinkedList<Action> {
+    private class ActionList extends ArrayList<Action> {
     }
 
     /**
@@ -1219,8 +1263,8 @@
     }
 
     /**
-     * Inflate the in-track view for the action of the given MIME-type. Will use
-     * the icon provided by the {@link DataKind}.
+     * Inflate the in-track view for the action of the given MIME-type, collapsing duplicate values.
+     * Will use the icon provided by the {@link DataKind}.
      */
     private View inflateAction(String mimeType) {
         final CheckableImageView view = (CheckableImageView)obtainView();
@@ -1228,6 +1272,9 @@
 
         // Add direct intent if single child, otherwise flag for multiple
         ActionList children = mActions.get(mimeType);
+        if (children.size() > 1) {
+            Collapser.collapseList(children);
+        }
         Action firstInfo = children.get(0);
         if (children.size() == 1) {
             view.setTag(firstInfo);