About Card
Change-Id: Idfe396d0a4fa24214599990c2895ed9569e5c0fa
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index b7b301d..7617128 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -24,38 +24,54 @@
import android.app.Activity;
import android.app.Fragment;
import android.app.LoaderManager.LoaderCallbacks;
+import android.app.SearchManager;
import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
import android.content.ContentUris;
import android.content.Intent;
import android.content.Loader;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.Color;
-import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
+import android.net.ParseException;
import android.net.Uri;
+import android.net.WebAddress;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Trace;
+import android.provider.CalendarContract;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.Event;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Identity;
+import android.provider.ContactsContract.CommonDataKinds.Im;
+import android.provider.ContactsContract.CommonDataKinds.Nickname;
+import android.provider.ContactsContract.CommonDataKinds.Note;
+import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.Relation;
import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.provider.ContactsContract.CommonDataKinds.Website;
import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.QuickContact;
import android.provider.ContactsContract.RawContacts;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
import android.util.Log;
+import android.util.Pair;
import android.view.Menu;
-import android.view.MenuItem;
import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.WindowManager;
@@ -66,6 +82,9 @@
import com.android.contacts.ContactSaveService;
import com.android.contacts.ContactsActivity;
import com.android.contacts.R;
+import com.android.contacts.common.CallUtil;
+import com.android.contacts.common.Collapser;
+import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.editor.SelectAccountDialogFragment;
import com.android.contacts.common.lettertiles.LetterTileDrawable;
import com.android.contacts.common.list.ShortcutIntentBuilder;
@@ -79,9 +98,18 @@
import com.android.contacts.common.model.dataitem.DataItem;
import com.android.contacts.common.model.dataitem.DataKind;
import com.android.contacts.common.model.dataitem.EmailDataItem;
+import com.android.contacts.common.model.dataitem.EventDataItem;
import com.android.contacts.common.model.dataitem.ImDataItem;
+import com.android.contacts.common.model.dataitem.NicknameDataItem;
+import com.android.contacts.common.model.dataitem.NoteDataItem;
+import com.android.contacts.common.model.dataitem.OrganizationDataItem;
import com.android.contacts.common.model.dataitem.PhoneDataItem;
-import com.android.contacts.common.util.DataStatus;
+import com.android.contacts.common.model.dataitem.RelationDataItem;
+import com.android.contacts.common.model.dataitem.SipAddressDataItem;
+import com.android.contacts.common.model.dataitem.StructuredNameDataItem;
+import com.android.contacts.common.model.dataitem.StructuredPostalDataItem;
+import com.android.contacts.common.model.dataitem.WebsiteDataItem;
+import com.android.contacts.common.util.DateUtils;
import com.android.contacts.detail.ContactDetailDisplayUtils;
import com.android.contacts.interactions.CalendarInteractionsLoader;
import com.android.contacts.interactions.CallLogInteractionsLoader;
@@ -91,16 +119,21 @@
import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
import com.android.contacts.quickcontact.ExpandingEntryCardView.ExpandingEntryCardViewListener;
import com.android.contacts.util.ImageViewDrawableSetter;
+import com.android.contacts.util.PhoneCapabilityTester;
import com.android.contacts.util.SchedulingUtils;
+import com.android.contacts.util.StructuredPostalUtils;
import com.android.contacts.widget.MultiShrinkScroller;
import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -146,17 +179,31 @@
private ImageView mPhotoView;
private View mTransparentView;
- private ExpandingEntryCardView mCommunicationCard;
+ private ExpandingEntryCardView mContactCard;
private ExpandingEntryCardView mRecentCard;
+ private ExpandingEntryCardView mAboutCard;
+ /**
+ * This list contains all the {@link DataItem}s. Each nested list contains all data items of a
+ * specific mimetype in sorted order, using mWithinMimeTypeDataItemComparator. The mimetype
+ * lists are sorted using mAmongstMimeTypeDataItemComparator.
+ */
+ private List<List<DataItem>> mDataItemsList;
+ /**
+ * A map between a mimetype string and the corresponding list of data items. The data items
+ * are in sorted order using mWithinMimeTypeDataItemComparator.
+ */
+ private Map<String, List<DataItem>> mDataItemsMap;
private MultiShrinkScroller mScroller;
private SelectAccountDialogFragmentListener mSelectAccountFragmentListener;
- private AsyncTask<Void, Void, Void> mEntriesAndActionsTask;
+ private AsyncTask<Void, Void, Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>
+ mEntriesAndActionsTask;
private ColorDrawable mWindowScrim;
private boolean mIsWaitingForOtherPieceOfExitAnimation;
private boolean mIsExitAnimationInProgress;
private boolean mHasComputedThemeColor;
+ private ComponentName mSmsComponent;
- private static final int MIN_NUM_COMMUNICATION_ENTRIES_SHOWN = 3;
+ private static final int MIN_NUM_CONTACT_ENTRIES_SHOWN = 3;
private static final int MIN_NUM_COLLAPSED_RECENT_ENTRIES_SHOWN = 3;
private Contact mContactData;
@@ -166,11 +213,6 @@
private final ImageViewDrawableSetter mPhotoSetter = new ImageViewDrawableSetter();
/**
- * Keeps the default action per mimetype. Empty if no default actions are set
- */
- private HashMap<String, Action> mDefaultsMap = new HashMap<String, Action>();
-
- /**
* {@link #LEADING_MIMETYPES} and {@link #TRAILING_MIMETYPES} are used to sort MIME-types.
*
* <p>The MIME-types in {@link #LEADING_MIMETYPES} appear in the front of the dialog,
@@ -188,6 +230,11 @@
private static final List<String> TRAILING_MIMETYPES = Lists.newArrayList(
StructuredPostal.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
+ private static final List<String> ABOUT_CARD_MIMETYPES = Lists.newArrayList(
+ Event.CONTENT_ITEM_TYPE, GroupMembership.CONTENT_ITEM_TYPE, Identity.CONTENT_ITEM_TYPE,
+ Im.CONTENT_ITEM_TYPE, Nickname.CONTENT_ITEM_TYPE, Note.CONTENT_ITEM_TYPE,
+ Organization.CONTENT_ITEM_TYPE, Relation.CONTENT_ITEM_TYPE, Website.CONTENT_ITEM_TYPE);
+
/** Id for the background contact loader */
private static final int LOADER_CONTACT_ID = 0;
@@ -226,7 +273,7 @@
@Override
public void onClick(View v) {
Log.i(TAG, "mEntryClickHandler onClick");
- Object intent = v.getTag();
+ final Object intent = v.getTag();
if (intent == null || !(intent instanceof Intent)) {
return;
}
@@ -316,6 +363,88 @@
}
};
+
+ /**
+ * Data items are compared to the same mimetype based off of three qualities:
+ * 1. Super primary
+ * 2. Primary
+ * 3. Times used
+ */
+ private final Comparator<DataItem> mWithinMimeTypeDataItemComparator =
+ new Comparator<DataItem>() {
+ @Override
+ public int compare(DataItem lhs, DataItem rhs) {
+ if (!lhs.getMimeType().equals(rhs.getMimeType())) {
+ Log.wtf(TAG, "Comparing DataItems with different mimetypes lhs.getMimeType(): " +
+ lhs.getMimeType() + " rhs.getMimeType(): " + rhs.getMimeType());
+ return 0;
+ }
+
+ if (lhs.isSuperPrimary()) {
+ return -1;
+ } else if (rhs.isSuperPrimary()) {
+ return 1;
+ } else if (lhs.isPrimary() && !rhs.isPrimary()) {
+ return -1;
+ } else if (!lhs.isPrimary() && rhs.isPrimary()) {
+ return 1;
+ } else {
+ final int lhsTimesUsed =
+ lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
+ final int rhsTimesUsed =
+ rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+
+ return rhsTimesUsed - lhsTimesUsed;
+ }
+ }
+ };
+
+ private final Comparator<List<DataItem>> mAmongstMimeTypeDataItemComparator =
+ new Comparator<List<DataItem>> () {
+ @Override
+ public int compare(List<DataItem> lhsList, List<DataItem> rhsList) {
+ DataItem lhs = lhsList.get(0);
+ DataItem rhs = rhsList.get(0);
+ final int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
+ final int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+ final int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
+ if (timesUsedDifference != 0) {
+ return timesUsedDifference;
+ }
+
+ final long lhsLastTimeUsed =
+ lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
+ final long rhsLastTimeUsed =
+ rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
+ final long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
+ if (lastTimeUsedDifference > 0) {
+ return 1;
+ } else if (lastTimeUsedDifference < 0) {
+ return -1;
+ }
+
+ // Times used and last time used are the same. Resort to statically defined.
+ final String lhsMimeType = lhs.getMimeType();
+ final String rhsMimeType = rhs.getMimeType();
+ for (String mimeType : LEADING_MIMETYPES) {
+ if (lhsMimeType.equals(mimeType)) {
+ return -1;
+ } else if (rhsMimeType.equals(mimeType)) {
+ return 1;
+ }
+ }
+ // Trailing types come last, so flip the returns
+ for (String mimeType : TRAILING_MIMETYPES) {
+ if (lhsMimeType.equals(mimeType)) {
+ return 1;
+ } else if (rhsMimeType.equals(mimeType)) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+ };
+
@Override
protected void onCreate(Bundle savedInstanceState) {
Trace.beginSection("onCreate()");
@@ -334,18 +463,23 @@
setContentView(R.layout.quickcontact_activity);
- mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
+ mSmsComponent = PhoneCapabilityTester.getSmsComponent(this);
+
+ mContactCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
mRecentCard = (ExpandingEntryCardView) findViewById(R.id.recent_card);
+ mAboutCard = (ExpandingEntryCardView) findViewById(R.id.about_card);
mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
- mCommunicationCard.setOnClickListener(mEntryClickHandler);
- mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
- mCommunicationCard.setExpandButtonText(
+ mContactCard.setOnClickListener(mEntryClickHandler);
+ mContactCard.setTitle(getResources().getString(R.string.communication_card_title));
+ mContactCard.setExpandButtonText(
getResources().getString(R.string.expanding_entry_card_view_see_all));
mRecentCard.setOnClickListener(mEntryClickHandler);
mRecentCard.setTitle(getResources().getString(R.string.recent_card_title));
+ mAboutCard.setOnClickListener(mEntryClickHandler);
+
mPhotoView = (ImageView) findViewById(R.id.photo);
mTransparentView = findViewById(R.id.transparent_view);
if (mScroller != null) {
@@ -520,8 +654,6 @@
mContactData = data;
invalidateOptionsMenu();
- mDefaultsMap.clear();
-
Trace.endSection();
Trace.beginSection("Set display photo & name");
@@ -532,28 +664,26 @@
Trace.endSection();
- // Maintain a list of phone numbers to pass into SmsInteractionsLoader
- final Set<String> phoneNumbers = new HashSet<>();
- // Maintain a list of email addresses to pass into CalendarInteractionsLoader
- final Set<String> emailAddresses = new HashSet<>();
- // List of Entry that makes up the ExpandingEntryCardView
- final List<Entry> entries = Lists.newArrayList();
+ mEntriesAndActionsTask = new AsyncTask<Void, Void,
+ Pair<List<List<DataItem>>, Map<String, List<DataItem>>>>() {
- mEntriesAndActionsTask = new AsyncTask<Void, Void, Void>() {
@Override
- protected Void doInBackground(Void... params) {
- computeEntriesAndActions(data, phoneNumbers, emailAddresses, entries);
- return null;
+ protected Pair<List<List<DataItem>>, Map<String, List<DataItem>>> doInBackground(
+ Void... params) {
+ return generateDataModelFromContact(data);
}
@Override
- protected void onPostExecute(Void aVoid) {
- super.onPostExecute(aVoid);
+ protected void onPostExecute(Pair<List<List<DataItem>>,
+ Map<String, List<DataItem>>> dataItemsPair) {
+ super.onPostExecute(dataItemsPair);
+ mDataItemsList = dataItemsPair.first;
+ mDataItemsMap = dataItemsPair.second;
// Check that original AsyncTask parameters are still valid and the activity
// is still running before binding to UI. A new intent could invalidate
// the results, for example.
if (data == mContactData && !isCancelled()) {
- bindEntriesAndActions(entries, phoneNumbers, emailAddresses);
+ bindDataToCards();
showActivity();
}
}
@@ -561,13 +691,24 @@
mEntriesAndActionsTask.execute();
}
- private void bindEntriesAndActions(List<Entry> entries,
- Set<String> phoneNumbers,
- Set<String> emailAddresses) {
- Trace.beginSection("start sms loader");
+ private void bindDataToCards() {
+ startInteractionLoaders();
+ populateContactAndAboutCard();
+ }
+
+ private void startInteractionLoaders() {
+ final List<DataItem> phoneDataItems = mDataItemsMap.get(Phone.CONTENT_ITEM_TYPE);
+ String[] phoneNumbers = null;
+ if (phoneDataItems != null) {
+ phoneNumbers = new String[phoneDataItems.size()];
+ for (int i = 0; i < phoneDataItems.size(); ++i) {
+ phoneNumbers[i] = ((PhoneDataItem) phoneDataItems.get(i)).getNumber();
+ }
+ }
final Bundle phonesExtraBundle = new Bundle();
- phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES,
- phoneNumbers.toArray(new String[phoneNumbers.size()]));
+ phonesExtraBundle.putStringArray(KEY_LOADER_EXTRA_PHONES, phoneNumbers);
+
+ Trace.beginSection("start sms loader");
getLoaderManager().initLoader(
LOADER_SMS_ID,
phonesExtraBundle,
@@ -581,27 +722,23 @@
mLoaderInteractionsCallbacks);
Trace.endSection();
+
Trace.beginSection("start calendar loader");
+ final List<DataItem> emailDataItems = mDataItemsMap.get(Email.CONTENT_ITEM_TYPE);
+ String[] emailAddresses = null;
+ if (emailDataItems != null) {
+ emailAddresses = new String[emailDataItems.size()];
+ for (int i = 0; i < emailDataItems.size(); ++i) {
+ emailAddresses[i] = ((EmailDataItem) emailDataItems.get(i)).getAddress();
+ }
+ }
final Bundle emailsExtraBundle = new Bundle();
- emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS,
- emailAddresses.toArray(new String[emailAddresses.size()]));
+ emailsExtraBundle.putStringArray(KEY_LOADER_EXTRA_EMAILS, emailAddresses);
getLoaderManager().initLoader(
LOADER_CALENDAR_ID,
emailsExtraBundle,
mLoaderInteractionsCallbacks);
Trace.endSection();
-
- Trace.beginSection("bind communicate card");
- if (entries.size() > 0) {
- mCommunicationCard.initialize(entries,
- /* numInitialVisibleEntries = */ MIN_NUM_COMMUNICATION_ENTRIES_SHOWN,
- /* isExpanded = */ false, mExpandingEntryCardViewListener);
- }
-
- final boolean hasData = !entries.isEmpty();
- mCommunicationCard.setVisibility(hasData ? View.VISIBLE : View.GONE);
-
- Trace.endSection();
}
private void showActivity() {
@@ -617,157 +754,322 @@
}
}
- private void computeEntriesAndActions(Contact data, Set<String> phoneNumbers,
- Set<String> emailAddresses, List<Entry> entries) {
- Trace.beginSection("inflate entries and actions");
+ private void populateContactAndAboutCard() {
+ Trace.beginSection("bind contact card");
- // Map from {@link String} MIME-type to a list of {@link Action}.
- final ActionMultiMap actions = new ActionMultiMap();
+ final List<Entry> contactCardEntries = new ArrayList<>();
+ final List<Entry> aboutCardEntries = new ArrayList<>();
+
+ int topContactIndex = 0;
+ for (int i = 0; i < mDataItemsList.size(); ++i) {
+ final List<DataItem> dataItemsByMimeType = mDataItemsList.get(i);
+ final DataItem topDataItem = dataItemsByMimeType.get(0);
+ if (ABOUT_CARD_MIMETYPES.contains(topDataItem.getMimeType())) {
+ aboutCardEntries.addAll(dataItemsToEntries(mDataItemsList.get(i)));
+ } else {
+ // Add most used to the top of the contact card
+ final Entry topEntry = dataItemToEntry(topDataItem);
+ if (topEntry != null) {
+ contactCardEntries.add(topContactIndex++, dataItemToEntry(topDataItem));
+ }
+ // TODO merge SMS into secondary action
+ if (topDataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phone = (PhoneDataItem) topDataItem;
+ Intent smsIntent = null;
+ if (mSmsComponent != null) {
+ smsIntent = new Intent(Intent.ACTION_SENDTO,
+ Uri.fromParts(CallUtil.SCHEME_SMSTO, phone.getNumber(), null));
+ smsIntent.setComponent(mSmsComponent);
+ }
+ contactCardEntries.add(topContactIndex++,
+ new Entry(
+ getResources().getDrawable(R.drawable.ic_message_24dp),
+ getResources().getString(R.string.send_message),
+ /* subHeader = */ null,
+ /* text = */ phone.buildDataString(
+ this, topDataItem.getDataKind()),
+ smsIntent,
+ /* isEditable = */ false));
+ }
+ // Add the rest of the entries to the bottom of the card
+ if (dataItemsByMimeType.size() > 1) {
+ contactCardEntries.addAll(dataItemsToEntries(
+ dataItemsByMimeType.subList(1, dataItemsByMimeType.size())));
+ }
+ }
+ }
+
+ if (contactCardEntries.size() > 0) {
+ mContactCard.initialize(contactCardEntries,
+ /* numInitialVisibleEntries = */ MIN_NUM_CONTACT_ENTRIES_SHOWN,
+ /* isExpanded = */ false,
+ mExpandingEntryCardViewListener);
+ mContactCard.setVisibility(View.VISIBLE);
+ } else {
+ mContactCard.setVisibility(View.GONE);
+ }
+ Trace.endSection();
+
+ Trace.beginSection("bind about card");
+ mAboutCard.initialize(aboutCardEntries,
+ /* numInitialVisibleEntries = */ 1,
+ /* isExpanded = */ true,
+ mExpandingEntryCardViewListener);
+ Trace.endSection();
+ }
+
+ /**
+ * Builds the {@link DataItem}s Map out of the Contact.
+ * @param data The contact to build the data from.
+ * @return A pair containing a list of data items sorted within mimetype and sorted
+ * amongst mimetype. The map goes from mimetype string to the sorted list of data items within
+ * mimetype
+ */
+ private Pair<List<List<DataItem>>, Map<String, List<DataItem>>> generateDataModelFromContact(
+ Contact data) {
+ Trace.beginSection("Build data items map");
+
+ final Map<String, List<DataItem>> dataItemsMap = new HashMap<>();
final ResolveCache cache = ResolveCache.getInstance(this);
for (RawContact rawContact : data.getRawContacts()) {
for (DataItem dataItem : rawContact.getDataItems()) {
+ dataItem.setRawContactId(rawContact.getId());
+
final String mimeType = dataItem.getMimeType();
+ if (mimeType == null) continue;
+
final AccountType accountType = rawContact.getAccountType(this);
final DataKind dataKind = AccountTypeManager.getInstance(this)
.getKindOrFallback(accountType, mimeType);
+ if (dataKind == null) continue;
- if (dataItem instanceof PhoneDataItem) {
- phoneNumbers.add(((PhoneDataItem) dataItem).getNormalizedNumber());
+ dataItem.setDataKind(dataKind);
+
+ final boolean hasData = !TextUtils.isEmpty(dataItem.buildDataString(this,
+ dataKind));
+
+ if (isMimeExcluded(mimeType) || !hasData) continue;
+
+ List<DataItem> dataItemListByType = dataItemsMap.get(mimeType);
+ if (dataItemListByType == null) {
+ dataItemListByType = new ArrayList<>();
+ dataItemsMap.put(mimeType, dataItemListByType);
}
-
- if (dataItem instanceof EmailDataItem) {
- emailAddresses.add(((EmailDataItem) dataItem).getAddress());
- }
-
- // Skip this data item if MIME-type excluded
- if (isMimeExcluded(mimeType)) continue;
-
- final long dataId = dataItem.getId();
- final boolean isPrimary = dataItem.isPrimary();
- final boolean isSuperPrimary = dataItem.isSuperPrimary();
-
- if (dataKind != null) {
- // Build an action for this data entry, find a mapping to a UI
- // element, build its summary from the cursor, and collect it
- // along with all others of this MIME-type.
- final Action action = new DataAction(getApplicationContext(),
- dataItem, dataKind);
- final boolean wasAdded = considerAdd(action, cache, isSuperPrimary, actions);
- if (wasAdded) {
- // Remember the default
- if (isSuperPrimary || (isPrimary && (mDefaultsMap.get(mimeType) == null))) {
- mDefaultsMap.put(mimeType, action);
- }
- }
- }
-
- // Handle Email rows with presence data as Im entry
- final DataStatus status = data.getStatuses().get(dataId);
- if (status != null && dataItem instanceof EmailDataItem) {
- final EmailDataItem email = (EmailDataItem) dataItem;
- final ImDataItem im = ImDataItem.createFromEmail(email);
- if (dataKind != null) {
- final DataAction action = new DataAction(getApplicationContext(),
- im, dataKind);
- action.setPresence(status.getPresence());
- considerAdd(action, cache, isSuperPrimary, actions);
- }
- }
+ dataItemListByType.add(dataItem);
}
}
-
Trace.endSection();
- Trace.beginSection("collapsing action list");
- Trace.endSection();
- Trace.beginSection("sort mimetypes");
-
+ Trace.beginSection("sort within mimetypes");
/*
* Sorting is a multi part step. The end result is to a have a sorted list of the most
- * used actions, one per mimetype. Then, within each mimetype, the list of actions for that
- * type is also sorted, based off of {super primary, primary, times used} in that order.
+ * used data items, one per mimetype. Then, within each mimetype, the list of data items
+ * for that type is also sorted, based off of {super primary, primary, times used} in that
+ * order.
*/
- final List<Action> topActions = new ArrayList<>();
- final List<Action> allActions = new ArrayList<>();
- for (List<Action> mimeTypeActions : actions.values()) {
- Collections.sort(mimeTypeActions, new Comparator<Action>() {
- @Override
- public int compare(Action lhs, Action rhs) {
- /*
- * Actions are compared to the same mimetype based off of three qualities:
- * 1. Super primary
- * 2. Primary
- * 3. Times used
- */
- if (lhs.isSuperPrimary()) {
- return -1;
- } else if (rhs.isSuperPrimary()) {
- return 1;
- } else if (lhs.isPrimary() && !rhs.isPrimary()) {
- return -1;
- } else if (!lhs.isPrimary() && rhs.isPrimary()) {
- return 1;
- } else {
- int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
- int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
+ final List<List<DataItem>> dataItemsList = new ArrayList<>();
+ for (List<DataItem> mimeTypeDataItems : dataItemsMap.values()) {
+ // Remove duplicate data items
+ Collapser.collapseList(mimeTypeDataItems, this);
+ // Sort within mimetype
+ Collections.sort(mimeTypeDataItems, mWithinMimeTypeDataItemComparator);
+ // Add to the list of data item lists
+ dataItemsList.add(mimeTypeDataItems);
+ }
+ Trace.endSection();
- return rhsTimesUsed - lhsTimesUsed;
- }
+ Trace.beginSection("sort amongst mimetypes");
+ // Sort amongst mimetypes to bubble up the top data items for the contact card
+ Collections.sort(dataItemsList, mAmongstMimeTypeDataItemComparator);
+ Trace.endSection();
+
+ return new Pair<>(dataItemsList, dataItemsMap);
+ }
+
+ /**
+ * Converts a {@link DataItem} into an {@link ExpandingEntryCardView.Entry} for display.
+ * If the {@link ExpandingEntryCardView.Entry} has no visual elements, null is returned.
+ * @param dataItem The {@link DataItem} to convert.
+ * @return The {@link ExpandingEntryCardView.Entry}, or null if no visual elements are present.
+ */
+ private Entry dataItemToEntry(DataItem dataItem) {
+ Drawable icon = null;
+ String header = null;
+ String subHeader = null;
+ Drawable subHeaderIcon = null;
+ String text = null;
+ Drawable textIcon = null;
+ Intent intent = null;
+ final boolean isEditable = false;
+
+ DataKind kind = dataItem.getDataKind();
+
+ if (dataItem instanceof ImDataItem) {
+ final ImDataItem im = (ImDataItem) dataItem;
+ intent = ContactsUtils.buildImIntent(this, im).first;
+ header = getResources().getString(R.string.header_im_entry);
+ final boolean isEmail = im.isCreatedFromEmail();
+ final int protocol = isEmail ? Im.PROTOCOL_GOOGLE_TALK : im.getProtocol();
+ subHeader = Im.getProtocolLabel(getResources(), protocol,
+ im.getCustomProtocol()).toString();
+ } else if (dataItem instanceof OrganizationDataItem) {
+ final OrganizationDataItem organization = (OrganizationDataItem) dataItem;
+ header = getResources().getString(R.string.header_organization_entry);
+ subHeader = organization.getCompany();
+ text = organization.getTitle();
+ } else if (dataItem instanceof NicknameDataItem) {
+ final NicknameDataItem nickname = (NicknameDataItem) dataItem;
+ // Build nickname entries
+ final boolean isNameRawContact =
+ (mContactData.getNameRawContactId() == dataItem.getRawContactId());
+
+ final boolean duplicatesTitle =
+ isNameRawContact
+ && mContactData.getDisplayNameSource() == DisplayNameSources.NICKNAME;
+
+ if (!duplicatesTitle) {
+ header = getResources().getString(R.string.header_nickname_entry);
+ subHeader = nickname.getName();
+ }
+ } else if (dataItem instanceof NoteDataItem) {
+ final NoteDataItem note = (NoteDataItem) dataItem;
+ header = getResources().getString(R.string.header_note_entry);
+ subHeader = note.getNote();
+ } else if (dataItem instanceof WebsiteDataItem) {
+ final WebsiteDataItem website = (WebsiteDataItem) dataItem;
+ header = getResources().getString(R.string.header_website_entry);
+ subHeader = website.getUrl();
+ try {
+ final WebAddress webAddress = new WebAddress(website.buildDataString(this, kind));
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(webAddress.toString()));
+ } catch (final ParseException e) {
+ Log.e(TAG, "Couldn't parse website: " + website.buildDataString(this, kind));
+ }
+ } else if (dataItem instanceof EventDataItem) {
+ final EventDataItem event = (EventDataItem) dataItem;
+ final String dataString = event.buildDataString(this, kind);
+ final Calendar cal = DateUtils.parseDate(dataString, false);
+ if (cal != null) {
+ final Date nextAnniversary =
+ DateUtils.getNextAnnualDate(cal);
+ final Uri.Builder builder = CalendarContract.CONTENT_URI.buildUpon();
+ builder.appendPath("time");
+ ContentUris.appendId(builder, nextAnniversary.getTime());
+ intent = new Intent(Intent.ACTION_VIEW).setData(builder.build());
+ }
+ header = getResources().getString(R.string.header_event_entry);
+ subHeader = getResources().getString(Event.getTypeResource(
+ event.getKindTypeColumn(kind)));
+ text = DateUtils.formatDate(this, dataString);
+ } else if (dataItem instanceof RelationDataItem) {
+ final RelationDataItem relation = (RelationDataItem) dataItem;
+ final String dataString = relation.buildDataString(this, kind);
+ if (!TextUtils.isEmpty(dataString)) {
+ intent = new Intent(Intent.ACTION_SEARCH);
+ intent.putExtra(SearchManager.QUERY, dataString);
+ intent.setType(Contacts.CONTENT_TYPE);
+ }
+ header = getResources().getString(R.string.header_relation_entry);
+ subHeader = relation.getName();
+ text = Relation.getTypeLabel(getResources(), relation.getKindTypeColumn(kind),
+ relation.getLabel()).toString();
+ } else if (dataItem instanceof PhoneDataItem) {
+ final PhoneDataItem phone = (PhoneDataItem) dataItem;
+ if (!TextUtils.isEmpty(phone.getNumber())) {
+ header = phone.buildDataString(this, kind);
+ text = Phone.getTypeLabel(getResources(), phone.getKindTypeColumn(kind),
+ phone.getLabel()).toString();
+ icon = getResources().getDrawable(R.drawable.ic_phone_24dp);
+ if (PhoneCapabilityTester.isPhone(this)) {
+ intent = CallUtil.getCallIntent(phone.getNumber());
}
- });
- topActions.add(mimeTypeActions.get(0));
- // Add all the other actions and remove the top one
- allActions.addAll(mimeTypeActions);
- allActions.remove(mimeTypeActions.get(0));
+ }
+ } else if (dataItem instanceof EmailDataItem) {
+ final EmailDataItem email = (EmailDataItem) dataItem;
+ final String address = email.getData();
+ if (!TextUtils.isEmpty(address)) {
+ final Uri mailUri = Uri.fromParts(CallUtil.SCHEME_MAILTO, address, null);
+ intent = new Intent(Intent.ACTION_SENDTO, mailUri);
+ header = email.getAddress();
+ text = Email.getTypeLabel(getResources(), email.getKindTypeColumn(kind),
+ email.getLabel()).toString();
+ icon = getResources().getDrawable(R.drawable.ic_email_24dp);
+ }
+ } else if (dataItem instanceof StructuredPostalDataItem) {
+ StructuredPostalDataItem postal = (StructuredPostalDataItem) dataItem;
+ final String postalAddress = postal.getFormattedAddress();
+ if (!TextUtils.isEmpty(postalAddress)) {
+ intent = StructuredPostalUtils.getViewPostalAddressIntent(postalAddress);
+ header = postal.getFormattedAddress();
+ text = StructuredPostal.getTypeLabel(getResources(), postal.getKindTypeColumn(kind),
+ postal.getLabel()).toString();
+ icon = getResources().getDrawable(R.drawable.ic_place_24dp);
+ }
+ } else if (dataItem instanceof SipAddressDataItem) {
+ if (PhoneCapabilityTester.isSipPhone(this)) {
+ final SipAddressDataItem sip = (SipAddressDataItem) dataItem;
+ final String address = sip.getSipAddress();
+ if (!TextUtils.isEmpty(address)) {
+ final Uri callUri = Uri.fromParts(CallUtil.SCHEME_SIP, address, null);
+ intent = CallUtil.getCallIntent(callUri);
+ // Note that this item will get a SIP-specific variant
+ // of the "call phone" icon, rather than the standard
+ // app icon for the Phone app (which we show for
+ // regular phone numbers.) That's because the phone
+ // app explicitly specifies an android:icon attribute
+ // for the SIP-related intent-filters in its manifest.
+ }
+ icon = ResolveCache.getInstance(this).getIcon(sip.getMimeType(), intent);
+ // Call mutate to create a new Drawable.ConstantState for color filtering
+ if (icon != null) {
+ icon.mutate();
+ }
+ }
+ } else if (dataItem instanceof StructuredNameDataItem) {
+ final String givenName = ((StructuredNameDataItem) dataItem).getGivenName();
+ if (!TextUtils.isEmpty(givenName)) {
+ mAboutCard.setTitle(getResources().getString(R.string.about_card_title) +
+ " " + givenName);
+ } else {
+ mAboutCard.setTitle(getResources().getString(R.string.about_card_title));
+ }
+ } else {
+ // Custom DataItem
+ header = dataItem.buildDataStringForDisplay(this, kind);
+ text = kind.typeColumn;
+ intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(Uri.parse(dataItem.buildDataString(this, kind)),
+ dataItem.getMimeType());
+ icon = ResolveCache.getInstance(this).getIcon(dataItem.getMimeType(), intent);
}
- // topActions now contains the top action for each mimetype. This list now needs to be
- // sorted, based off of {times used, last used, statically defined} in that order.
- Collections.sort(topActions, new Comparator<Action>() {
- @Override
- public int compare(Action lhs, Action rhs) {
- int lhsTimesUsed = lhs.getTimesUsed() == null ? 0 : lhs.getTimesUsed();
- int rhsTimesUsed = rhs.getTimesUsed() == null ? 0 : rhs.getTimesUsed();
- int timesUsedDifference = rhsTimesUsed - lhsTimesUsed;
- if (timesUsedDifference != 0) {
- return timesUsedDifference;
- }
-
- long lhsLastTimeUsed = lhs.getLastTimeUsed() == null ? 0 : lhs.getLastTimeUsed();
- long rhsLastTimeUsed = rhs.getLastTimeUsed() == null ? 0 : rhs.getLastTimeUsed();
- long lastTimeUsedDifference = rhsLastTimeUsed - lhsLastTimeUsed;
- if (lastTimeUsedDifference > 0) {
- return 1;
- } else if (lastTimeUsedDifference < 0) {
- return -1;
- }
-
- // Times used and last time used are the same. Resort to statically defined.
- String lhsMimeType = lhs.getMimeType();
- String rhsMimeType = rhs.getMimeType();
- for (String mimeType : LEADING_MIMETYPES) {
- if (lhsMimeType.equals(mimeType)) {
- return -1;
- } else if (rhsMimeType.equals(mimeType)) {
- return 1;
- }
- }
- // Trailing types come last, so flip the returns
- for (String mimeType : TRAILING_MIMETYPES) {
- if (lhsMimeType.equals(mimeType)) {
- return 1;
- } else if (rhsMimeType.equals(mimeType)) {
- return -1;
- }
- }
- return 0;
+ if (intent != null) {
+ // Do not set the intent is there are no resolves
+ if (!PhoneCapabilityTester.isIntentRegistered(this, intent)) {
+ intent = null;
}
- });
+ }
- entries.addAll(actionsToEntries(topActions));
- entries.addAll(actionsToEntries(allActions));
- Trace.endSection();
+ // If the Entry has no visual elements, return null
+ if (icon == null && TextUtils.isEmpty(header) && TextUtils.isEmpty(subHeader) &&
+ subHeaderIcon == null && TextUtils.isEmpty(text) && textIcon == null) {
+ return null;
+ }
+
+ return new Entry(icon, header, subHeader, subHeaderIcon, text, textIcon,
+ intent, isEditable);
+ }
+
+ private List<Entry> dataItemsToEntries(List<DataItem> dataItems) {
+ final List<Entry> entries = new ArrayList<>();
+ for (DataItem dataItem : dataItems) {
+ final Entry entry = dataItemToEntry(dataItem);
+ if (entry != null) {
+ entries.add(entry);
+ }
+ }
+ return entries;
}
/**
@@ -856,8 +1158,9 @@
mColorFilter =
new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
- mCommunicationCard.setColorAndFilter(color, mColorFilter);
+ mContactCard.setColorAndFilter(color, mColorFilter);
mRecentCard.setColorAndFilter(color, mColorFilter);
+ mAboutCard.setColorAndFilter(color, mColorFilter);
}
private void updateStatusBarColor() {
@@ -891,65 +1194,8 @@
return 0;
}
- /**
- * Consider adding the given {@link Action}, which will only happen if
- * {@link PackageManager} finds an application to handle
- * {@link Action#getIntent()}.
- * @param action the action to handle
- * @param resolveCache cache of applications that can handle actions
- * @param front indicates whether to add the action to the front of the list
- * @param actions where to put the action.
- * @return true if action has been added
- */
- private boolean considerAdd(Action action, ResolveCache resolveCache, boolean front,
- ActionMultiMap actions) {
- if (resolveCache.hasResolve(action)) {
- actions.put(action.getMimeType(), action, front);
- return true;
- }
- return false;
- }
-
- /**
- * Converts a list of Action into a list of Entry
- * @param actions The list of Action to convert
- * @return The converted list of Entry
- */
- private List<Entry> actionsToEntries(List<Action> actions) {
- List<Entry> entries = new ArrayList<>();
- for (Action action : actions) {
- final String header = action.getBody() == null ? null : action.getBody().toString();
- final String footer = action.getBody() == null ? null : action.getBody().toString();
- String body = null;
- Drawable icon = null;
- switch (action.getMimeType()) {
- case Phone.CONTENT_ITEM_TYPE:
- icon = getResources().getDrawable(R.drawable.ic_phone_24dp);
- break;
- case Email.CONTENT_ITEM_TYPE:
- icon = getResources().getDrawable(R.drawable.ic_email_24dp);
- break;
- case StructuredPostal.CONTENT_ITEM_TYPE:
- icon = getResources().getDrawable(R.drawable.ic_place_24dp);
- break;
- default:
- icon = ResolveCache.getInstance(this).getIcon(action);
- }
- entries.add(new Entry(icon, header, body, footer, action.getIntent(),
- /* isEditable= */ false));
-
- // Add SMS in addition to phone calls
- if (action.getMimeType().equals(Phone.CONTENT_ITEM_TYPE)) {
- entries.add(new Entry(getResources().getDrawable(R.drawable.ic_message_24dp),
- getResources().getString(R.string.send_message), null, header,
- action.getAlternateIntent(), /* isEditable = */ false));
- }
- }
- return entries;
- }
-
private List<Entry> contactInteractionsToEntries(List<ContactInteraction> interactions) {
- List<Entry> entries = new ArrayList<>();
+ final List<Entry> entries = new ArrayList<>();
for (ContactInteraction interaction : interactions) {
entries.add(new Entry(interaction.getIcon(this),
interaction.getViewHeader(this),
@@ -963,7 +1209,7 @@
return entries;
}
- private LoaderCallbacks<Contact> mLoaderContactCallbacks =
+ private final LoaderCallbacks<Contact> mLoaderContactCallbacks =
new LoaderCallbacks<Contact>() {
@Override
public void onLoaderReset(Loader<Contact> loader) {
@@ -1029,7 +1275,7 @@
overridePendingTransition(0, 0);
}
- private LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
+ private final LoaderCallbacks<List<ContactInteraction>> mLoaderInteractionsCallbacks =
new LoaderCallbacks<List<ContactInteraction>>() {
@Override
@@ -1091,7 +1337,7 @@
}
private void bindRecentData() {
- List<ContactInteraction> allInteractions = new ArrayList<>();
+ final List<ContactInteraction> allInteractions = new ArrayList<>();
for (List<ContactInteraction> loaderInteractions : mRecentLoaderResults.values()) {
allInteractions.addAll(loaderInteractions);
}
@@ -1110,6 +1356,15 @@
/* isExpanded = */ false, mExpandingEntryCardViewListener);
mRecentCard.setVisibility(View.VISIBLE);
}
+
+ // About card is initialized along with the contact card, but since it appears after
+ // the recent card in the UI, we hold off until making it visible until the recent card
+ // is also ready to avoid stuttering.
+ if (mAboutCard.shouldShow()) {
+ mAboutCard.setVisibility(View.VISIBLE);
+ } else {
+ mAboutCard.setVisibility(View.GONE);
+ }
}
@Override
@@ -1153,7 +1408,7 @@
!isStarred);
// Now perform the real save
- Intent intent = ContactSaveService.createSetStarredIntent(
+ final Intent intent = ContactSaveService.createSetStarredIntent(
QuickContactActivity.this, mLookupUri, !isStarred);
startService(intent);
}
@@ -1197,7 +1452,7 @@
try {
this.startActivity(chooseIntent);
- } catch (ActivityNotFoundException ex) {
+ } catch (final ActivityNotFoundException ex) {
Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
}
}
@@ -1229,7 +1484,7 @@
@Override
public boolean onCreateOptionsMenu(Menu menu) {
- MenuInflater inflater = getMenuInflater();
+ final MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.quickcontact, menu);
return true;
}