Implements data collapsing in the contact card.
Provides a Collapsible interface that can implemented used by any class
whose data items can be collapsed upon one another. Also provides a
utility function for collapsing a list of items into a list of collapsed
items.
Uses this interface to collapse data items in the contact view card.
ViewEntrys with the same data, mimetype, intent action, auxIntent action
and action item are collapsed and shown as a single item. If the user
makes this item default, all data rows represented will be made primary,
and one will be chosen (arbitrarily) to be super primary.
diff --git a/src/com/android/contacts/Collapser.java b/src/com/android/contacts/Collapser.java
new file mode 100644
index 0000000..db1da1f
--- /dev/null
+++ b/src/com/android/contacts/Collapser.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2009 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;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.ArrayList;
+
+/**
+ * Class used for collapsing data items into groups of similar items. The data items that should be
+ * collapsible should implement the Collapsible interface. The class also contains a utility
+ * function that takes an ArrayList of items and returns a list of the same items collapsed into
+ * groups.
+ */
+public final class Collapser {
+
+ /*
+ * This utility class cannot be instantiated.
+ */
+ private Collapser() {}
+
+ /*
+ * Interface implemented by data types that can be collapsed into groups of similar data. This
+ * can be used for example to collapse similar contact data items into a single item.
+ */
+ public interface Collapsible<T> {
+ public boolean collapseWith(T t);
+ public String getCollapseKey();
+ }
+
+ /**
+ * Collapses a list of Collapsible items into a list of collapsed items. Items are collapsed
+ * if they produce equal collapseKeys {@Link Collapsible#getCollapseKey()}, and are collapsed
+ * through the {@Link Collapsible#doCollapseWith(Object)} function implemented by the data item.
+ *
+ * @param list ArrayList of Objects of type <T extends Collapsible<T>> to be collapsed.
+ */
+ public static <T extends Collapsible<T>> void collapseList(ArrayList<T> list) {
+ HashMap<String, T> collapseMap = new HashMap<String, T>();
+ ArrayList<String> collapseKeys = new ArrayList<String>();
+
+ int listSize = list.size();
+ for (int j = 0; j < listSize; j++) {
+ T entry = list.get(j);
+ String collapseKey = entry.getCollapseKey();
+ if (!collapseMap.containsKey(collapseKey)) {
+ collapseMap.put(collapseKey, entry);
+ collapseKeys.add(collapseKey);
+ } else {
+ collapseMap.get(collapseKey).collapseWith(entry);
+ }
+ }
+
+ if (collapseKeys.size() < listSize) {
+ list.clear();
+ Iterator<String> itr = collapseKeys.iterator();
+ while (itr.hasNext()) {
+ list.add(collapseMap.get(itr.next()));
+ }
+ }
+ }
+}
diff --git a/src/com/android/contacts/ContactEntryAdapter.java b/src/com/android/contacts/ContactEntryAdapter.java
index 2f8b109..11c862a 100644
--- a/src/com/android/contacts/ContactEntryAdapter.java
+++ b/src/com/android/contacts/ContactEntryAdapter.java
@@ -80,6 +80,7 @@
* Base class for adapter entries.
*/
public static class Entry {
+ public int type = -1;
public String label;
public String data;
public Uri uri;
@@ -92,6 +93,7 @@
* Helper for making subclasses parcelable.
*/
protected void writeToParcel(Parcel p) {
+ p.writeInt(type);
p.writeString(label);
p.writeString(data);
p.writeParcelable(uri, 0);
@@ -104,6 +106,7 @@
* Helper for making subclasses parcelable.
*/
protected void readFromParcel(Parcel p) {
+ type = p.readInt();
label = p.readString();
data = p.readString();
uri = p.readParcelable(null);
diff --git a/src/com/android/contacts/EditContactActivity.java b/src/com/android/contacts/EditContactActivity.java
index edcb178..32b55ef 100644
--- a/src/com/android/contacts/EditContactActivity.java
+++ b/src/com/android/contacts/EditContactActivity.java
@@ -199,23 +199,6 @@
/* package */ static final int MSG_ADD_EMAIL = 4;
/* package */ static final int MSG_ADD_POSTAL = 5;
- private static final int[] TYPE_PRECEDENCE_PHONES = new int[] {
- Phone.TYPE_MOBILE, Phone.TYPE_HOME, Phone.TYPE_WORK, Phone.TYPE_OTHER
- };
- private static final int[] TYPE_PRECEDENCE_EMAIL = new int[] {
- Email.TYPE_HOME, Email.TYPE_WORK, Email.TYPE_OTHER
- };
- private static final int[] TYPE_PRECEDENCE_POSTAL = new int[] {
- Postal.TYPE_HOME, Postal.TYPE_WORK, Postal.TYPE_OTHER
- };
- private static final int[] TYPE_PRECEDENCE_IM = new int[] {
- Im.PROTOCOL_GOOGLE_TALK, Im.PROTOCOL_AIM, Im.PROTOCOL_MSN, Im.PROTOCOL_YAHOO,
- Im.PROTOCOL_JABBER
- };
- private static final int[] TYPE_PRECEDENCE_ORG = new int[] {
- Organization.TYPE_WORK, Organization.TYPE_OTHER
- };
-
public void onClick(View v) {
switch (v.getId()) {
case R.id.photoImage: {
@@ -632,6 +615,8 @@
return precedenceList[precedenceList.length - 1];
}
+ // TODO When this gets brought back we'll need to use the new TypePrecedence class instead of
+ // the older local TYPE_PRECEDENCE* contstants.
private void doAddAction(int sectionType) {
EditEntry entry = null;
switch (sectionType) {
diff --git a/src/com/android/contacts/FastTrackWindow.java b/src/com/android/contacts/FastTrackWindow.java
index b96cf16..f61b73e 100644
--- a/src/com/android/contacts/FastTrackWindow.java
+++ b/src/com/android/contacts/FastTrackWindow.java
@@ -143,6 +143,8 @@
*/
private ActionMap mActions = new ActionMap();
+ // TODO We should move this to someplace more general as it is needed in a few places in the app
+ // code.
/**
* Specific mime-type for {@link Phone#CONTENT_ITEM_TYPE} entries that
* distinguishes actions that should initiate a text message.
diff --git a/src/com/android/contacts/TypePrecedence.java b/src/com/android/contacts/TypePrecedence.java
new file mode 100644
index 0000000..25d76e7
--- /dev/null
+++ b/src/com/android/contacts/TypePrecedence.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2009 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;
+
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Postal;
+
+/**
+ * This class contains utility functions for determining the precedence of different types
+ * associated with contact data items.
+ */
+public final class TypePrecedence {
+
+ /* This utility class has cannot be instantiated.*/
+ private TypePrecedence() {}
+
+ //TODO These may need to be tweaked.
+ private static final int[] TYPE_PRECEDENCE_PHONES = {
+ Phone.TYPE_CUSTOM,
+ Phone.TYPE_MOBILE,
+ Phone.TYPE_HOME,
+ Phone.TYPE_WORK,
+ Phone.TYPE_OTHER,
+ Phone.TYPE_FAX_HOME,
+ Phone.TYPE_FAX_WORK,
+ Phone.TYPE_PAGER};
+
+ private static final int[] TYPE_PRECEDENCE_EMAIL = {
+ Email.TYPE_CUSTOM,
+ Email.TYPE_HOME,
+ Email.TYPE_WORK,
+ Email.TYPE_OTHER};
+
+ private static final int[] TYPE_PRECEDENCE_POSTAL = {
+ Postal.TYPE_CUSTOM,
+ Postal.TYPE_HOME,
+ Postal.TYPE_WORK,
+ Postal.TYPE_OTHER};
+
+ private static final int[] TYPE_PRECEDENCE_IM = {
+ Im.TYPE_CUSTOM,
+ Im.TYPE_HOME,
+ Im.TYPE_WORK,
+ Im.TYPE_OTHER};
+
+ private static final int[] TYPE_PRECEDENCE_ORG = {
+ Organization.TYPE_CUSTOM,
+ Organization.TYPE_WORK,
+ Organization.TYPE_HOME,
+ Organization.TYPE_OTHER};
+
+ /**
+ * Returns the precedence (1 being the highest) of a type in the context of it's mimetype.
+ *
+ * @param mimetype The mimetype of the data with which the type is associated.
+ * @param type The integer type as defined in {@Link ContactsContract#CommonDataKinds}.
+ * @return The integer precedence, where 1 is the highest.
+ */
+ public static int getTypePrecedence(String mimetype, int type) {
+ int[] typePrecedence = getTypePrecedenceList(mimetype);
+ if (typePrecedence == null) {
+ return -1;
+ }
+
+ for (int i = 0; i < typePrecedence.length; i++) {
+ if (typePrecedence[i] == type) {
+ return i;
+ }
+ }
+ return typePrecedence.length;
+ }
+
+ private static int[] getTypePrecedenceList(String mimetype) {
+ if (mimetype.equals(Phone.CONTENT_ITEM_TYPE)) {
+ return TYPE_PRECEDENCE_PHONES;
+ } else if (mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+ return TYPE_PRECEDENCE_PHONES;
+ } else if (mimetype.equals(Email.CONTENT_ITEM_TYPE)) {
+ return TYPE_PRECEDENCE_EMAIL;
+ } else if (mimetype.equals(Postal.CONTENT_ITEM_TYPE)) {
+ return TYPE_PRECEDENCE_POSTAL;
+ } else if (mimetype.equals(Im.CONTENT_ITEM_TYPE)) {
+ return TYPE_PRECEDENCE_IM;
+ } else if (mimetype.equals(Organization.CONTENT_ITEM_TYPE)) {
+ return TYPE_PRECEDENCE_ORG;
+ } else {
+ return null;
+ }
+ }
+
+
+}
diff --git a/src/com/android/contacts/ViewContactActivity.java b/src/com/android/contacts/ViewContactActivity.java
index ae5c252..6414748 100644
--- a/src/com/android/contacts/ViewContactActivity.java
+++ b/src/com/android/contacts/ViewContactActivity.java
@@ -29,6 +29,7 @@
import static com.android.contacts.ContactEntryAdapter.DATA_IS_SUPER_PRIMARY_COLUMN;
import static com.android.contacts.ContactEntryAdapter.DATA_MIMETYPE_COLUMN;
+import com.android.contacts.Collapser.Collapsible;
import com.android.contacts.SplitAggregateView.OnContactSelectedListener;
import com.android.internal.telephony.ITelephony;
@@ -61,6 +62,9 @@
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Presence;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
@@ -79,6 +83,9 @@
import android.widget.Toast;
import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
/**
* Displays the details of a specific contact.
@@ -282,6 +289,13 @@
// Build up the contact entries
buildEntries(mCursor);
+
+ // Collapse similar data items in select sections.
+ Collapser.collapseList(mPhoneEntries);
+ Collapser.collapseList(mSmsEntries);
+ Collapser.collapseList(mEmailEntries);
+ Collapser.collapseList(mPostalEntries);
+
if (mAdapter == null) {
mAdapter = new ViewAdapter(this, mSections);
setListAdapter(mAdapter);
@@ -468,9 +482,16 @@
// Update the primary values in the data record.
ContentValues values = new ContentValues(2);
values.put(Data.IS_PRIMARY, 1);
- values.put(Data.IS_SUPER_PRIMARY, 1);
- Log.i(TAG, mUri.toString());
+ if (entry.ids.size() > 0) {
+ for (int i = 0; i < entry.ids.size(); i++) {
+ getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI,
+ entry.ids.get(i)),
+ values, null, null);
+ }
+ }
+
+ values.put(Data.IS_SUPER_PRIMARY, 1);
getContentResolver().update(ContentUris.withAppendedId(Data.CONTENT_URI, entry.id),
values, null, null);
dataChanged();
@@ -699,6 +720,8 @@
final boolean isSuperPrimary = "1".equals(
aggCursor.getString(DATA_IS_SUPER_PRIMARY_COLUMN));
+ entry.type = type;
+
// Don't crash if the data is bogus
if (TextUtils.isEmpty(data)) {
Log.w(TAG, "empty data for contact method " + id);
@@ -712,7 +735,7 @@
final CharSequence displayLabel = ContactsUtils.getDisplayLabel(
this, mimetype, type, label);
entry.label = buildActionString(R.string.actionCall, displayLabel, true);
- entry.data = data;
+ entry.data = PhoneNumberUtils.stripSeparators(data);
entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, entry.uri);
entry.auxIntent = new Intent(Intent.ACTION_SENDTO,
Uri.fromParts("sms", data, null));
@@ -728,11 +751,12 @@
ViewEntry smsEntry = new ViewEntry();
smsEntry.label = buildActionString(
R.string.actionText, displayLabel, true);
- smsEntry.data = data;
+ smsEntry.data = PhoneNumberUtils.stripSeparators(data);
smsEntry.id = id;
smsEntry.uri = uri;
smsEntry.intent = entry.auxIntent;
smsEntry.mimetype = FastTrackWindow.MIME_SMS_ADDRESS;
+ smsEntry.type = type;
smsEntry.actionIcon = R.drawable.sym_action_sms;
mSmsEntries.add(smsEntry);
}
@@ -787,10 +811,11 @@
constructImToUrl(host, data));
}
entry.data = data;
- //TODO(emillar) Do we want presence info?
+ //TODO(emillar) Add in presence info
/*if (!aggCursor.isNull(METHODS_STATUS_COLUMN)) {
entry.presenceIcon = Presence.getPresenceIconResourceId(
aggCursor.getInt(METHODS_STATUS_COLUMN));
+ entry.status = ...
}*/
entry.actionIcon = android.R.drawable.sym_action_chat;
mImEntries.add(entry);
@@ -904,7 +929,6 @@
}
}
-
String buildActionString(int actionResId, CharSequence type, boolean lowerCase) {
// If there is no type just display an empty string
if (type == null) {
@@ -921,13 +945,63 @@
/**
* A basic structure with the data for a contact entry in the list.
*/
- final static class ViewEntry extends ContactEntryAdapter.Entry {
+ static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
public int primaryIcon = -1;
public Intent intent;
public Intent auxIntent = null;
- public int presenceIcon = -1;
+ public int status = -1;
public int actionIcon = -1;
public int maxLabelLines = 1;
+ public ArrayList<Long> ids = new ArrayList<Long>();
+ public int collapseCount = 0;
+
+ public boolean collapseWith(ViewEntry entry) {
+ // assert equal collapse keys
+ if (!getCollapseKey().equals(entry.getCollapseKey())) {
+ return false;
+ }
+
+ // Choose the label associated with the highest type precedence.
+ if (TypePrecedence.getTypePrecedence(mimetype, type)
+ > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
+ type = entry.type;
+ label = entry.label;
+ }
+
+ // Choose the max of the maxLines and maxLabelLines values.
+ maxLines = Math.max(maxLines, entry.maxLines);
+ maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
+
+ // Choose the presence with the highest precedence.
+ if (Presence.getPresencePrecedence(status)
+ < Presence.getPresencePrecedence(entry.status)) {
+ status = entry.status;
+ }
+
+ // If any of the collapsed entries are primary make the whole thing primary.
+ if (primaryIcon != entry.primaryIcon && primaryIcon == -1) {
+ primaryIcon = entry.primaryIcon;
+ }
+
+ // uri, and contactdId, shouldn't make a difference. Just keep the original.
+
+ // Keep track of all the ids that have been collapsed with this one.
+ ids.add(entry.id);
+ collapseCount++;
+ return true;
+ }
+
+ public String getCollapseKey() {
+ StringBuilder hashSb = new StringBuilder();
+ hashSb.append(data);
+ hashSb.append(mimetype);
+ hashSb.append((intent != null && intent.getAction() != null)
+ ? intent.getAction() : "");
+ hashSb.append((auxIntent != null && auxIntent.getAction() != null)
+ ? auxIntent.getAction() : "");
+ hashSb.append(actionIcon);
+ return hashSb.toString();
+ }
}
private static final class ViewAdapter extends ContactEntryAdapter<ViewEntry> {
@@ -997,7 +1071,12 @@
// Set the data
TextView data = views.data;
if (data != null) {
- data.setText(entry.data);
+ if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
+ || entry.mimetype.equals(FastTrackWindow.MIME_SMS_ADDRESS)) {
+ data.setText(PhoneNumberUtils.formatNumber(entry.data));
+ } else {
+ data.setText(entry.data);
+ }
setMaxLines(data, entry.maxLines);
}
@@ -1015,8 +1094,9 @@
Drawable presenceIcon = null;
if (entry.primaryIcon != -1) {
presenceIcon = resources.getDrawable(entry.primaryIcon);
- } else if (entry.presenceIcon != -1) {
- presenceIcon = resources.getDrawable(entry.presenceIcon);
+ } else if (entry.status != -1) {
+ presenceIcon = resources.getDrawable(
+ Presence.getPresenceIconResourceId(entry.status));
}
ImageView presence = views.presenceIcon;