Home | History | Annotate | Download | only in fragments
      1 /*
      2  * Copyright (C) 2010 Google Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *	    http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.loaderapp.fragments;
     18 
     19 import com.android.loaderapp.ContactHeaderWidget;
     20 import com.android.loaderapp.R;
     21 import com.android.loaderapp.model.Collapser;
     22 import com.android.loaderapp.model.ContactLoader;
     23 import com.android.loaderapp.model.ContactsSource;
     24 import com.android.loaderapp.model.Sources;
     25 import com.android.loaderapp.model.TypePrecedence;
     26 import com.android.loaderapp.model.Collapser.Collapsible;
     27 import com.android.loaderapp.model.ContactLoader.ContactData;
     28 import com.android.loaderapp.model.ContactsSource.DataKind;
     29 import com.android.loaderapp.util.Constants;
     30 import com.android.loaderapp.util.ContactPresenceIconUtil;
     31 import com.android.loaderapp.util.ContactsUtils;
     32 import com.android.loaderapp.util.DataStatus;
     33 import com.google.android.collect.Lists;
     34 import com.google.android.collect.Maps;
     35 
     36 import android.app.LoaderManagingFragment;
     37 import android.content.ActivityNotFoundException;
     38 import android.content.ContentUris;
     39 import android.content.ContentValues;
     40 import android.content.Context;
     41 import android.content.Entity;
     42 import android.content.Intent;
     43 import android.content.Loader;
     44 import android.content.Entity.NamedContentValues;
     45 import android.content.res.Resources;
     46 import android.graphics.drawable.Drawable;
     47 import android.net.ParseException;
     48 import android.net.Uri;
     49 import android.net.WebAddress;
     50 import android.os.Bundle;
     51 import android.provider.ContactsContract.CommonDataKinds;
     52 import android.provider.ContactsContract.Contacts;
     53 import android.provider.ContactsContract.Data;
     54 import android.provider.ContactsContract.DisplayNameSources;
     55 import android.provider.ContactsContract.RawContacts;
     56 import android.provider.ContactsContract.StatusUpdates;
     57 import android.provider.ContactsContract.CommonDataKinds.Email;
     58 import android.provider.ContactsContract.CommonDataKinds.Im;
     59 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     60 import android.provider.ContactsContract.CommonDataKinds.Note;
     61 import android.provider.ContactsContract.CommonDataKinds.Organization;
     62 import android.provider.ContactsContract.CommonDataKinds.Phone;
     63 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     64 import android.provider.ContactsContract.CommonDataKinds.Website;
     65 import android.telephony.PhoneNumberUtils;
     66 import android.text.TextUtils;
     67 import android.util.Log;
     68 import android.view.LayoutInflater;
     69 import android.view.View;
     70 import android.view.ViewGroup;
     71 import android.view.View.OnClickListener;
     72 import android.widget.AdapterView;
     73 import android.widget.ImageView;
     74 import android.widget.ListView;
     75 import android.widget.TextView;
     76 import android.widget.AdapterView.OnItemClickListener;
     77 
     78 import java.util.ArrayList;
     79 import java.util.HashMap;
     80 
     81 public class ContactFragment extends LoaderManagingFragment<ContactData>
     82         implements OnClickListener, OnItemClickListener {
     83     private static final String TAG = "ContactCoupler";
     84 
     85     static final String ARG_URI = "uri";
     86     static final int LOADER_DETAILS = 1;
     87 
     88     Uri mUri;
     89 
     90     private static final boolean SHOW_SEPARATORS = false;
     91 
     92     protected Uri mLookupUri;
     93     private ViewAdapter mAdapter;
     94     private int mNumPhoneNumbers = 0;
     95     private Controller mController;
     96 
     97     /**
     98      * A list of distinct contact IDs included in the current contact.
     99      */
    100     private ArrayList<Long> mRawContactIds = new ArrayList<Long>();
    101 
    102     /* package */ ArrayList<ViewEntry> mPhoneEntries = new ArrayList<ViewEntry>();
    103     /* package */ ArrayList<ViewEntry> mSmsEntries = new ArrayList<ViewEntry>();
    104     /* package */ ArrayList<ViewEntry> mEmailEntries = new ArrayList<ViewEntry>();
    105     /* package */ ArrayList<ViewEntry> mPostalEntries = new ArrayList<ViewEntry>();
    106     /* package */ ArrayList<ViewEntry> mImEntries = new ArrayList<ViewEntry>();
    107     /* package */ ArrayList<ViewEntry> mNicknameEntries = new ArrayList<ViewEntry>();
    108     /* package */ ArrayList<ViewEntry> mOrganizationEntries = new ArrayList<ViewEntry>();
    109     /* package */ ArrayList<ViewEntry> mGroupEntries = new ArrayList<ViewEntry>();
    110     /* package */ ArrayList<ViewEntry> mOtherEntries = new ArrayList<ViewEntry>();
    111     /* package */ ArrayList<ArrayList<ViewEntry>> mSections = new ArrayList<ArrayList<ViewEntry>>();
    112 
    113     protected ContactHeaderWidget mContactHeaderWidget;
    114 
    115     protected LayoutInflater mInflater;
    116 
    117     protected int mReadOnlySourcesCnt;
    118     protected int mWritableSourcesCnt;
    119     protected boolean mAllRestricted;
    120 
    121     protected Uri mPrimaryPhoneUri = null;
    122 
    123     protected ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
    124 
    125     private long mNameRawContactId = -1;
    126     private int mDisplayNameSource = DisplayNameSources.UNDEFINED;
    127 
    128     private ArrayList<Entity> mEntities = Lists.newArrayList();
    129     private HashMap<Long, DataStatus> mStatuses = Maps.newHashMap();
    130 
    131     /**
    132      * The view shown if the detail list is empty.
    133      * We set this to the list view when first bind the adapter, so that it won't be shown while
    134      * we're loading data.
    135      */
    136     private View mEmptyView;
    137 
    138     private ListView mListView;
    139     private boolean mShowSmsLinksForAllPhones;
    140 
    141     public ContactFragment() {
    142     }
    143 
    144     public ContactFragment(Uri uri, ContactFragment.Controller controller) {
    145         mUri = uri;
    146         mController = controller;
    147     }
    148 
    149     @Override
    150     public void onCreate(Bundle savedState) {
    151         super.onCreate(savedState);
    152     }
    153 
    154     @Override
    155     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
    156         View view = inflater.inflate(R.layout.contact_details, container, false);
    157 
    158         mInflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    159 
    160         mContactHeaderWidget = (ContactHeaderWidget) view.findViewById(R.id.contact_header_widget);
    161         mContactHeaderWidget.setExcludeMimes(new String[] { Contacts.CONTENT_ITEM_TYPE });
    162 
    163         mListView = (ListView) view.findViewById(android.R.id.list);
    164         mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    165         mListView.setOnItemClickListener(this);
    166         // Don't set it to mListView yet.  We do so later when we bind the adapter.
    167         mEmptyView = view.findViewById(android.R.id.empty);
    168 
    169         // Build the list of sections. The order they're added to mSections dictates the
    170         // order they are displayed in the list.
    171         mSections.add(mPhoneEntries);
    172         mSections.add(mSmsEntries);
    173         mSections.add(mEmailEntries);
    174         mSections.add(mImEntries);
    175         mSections.add(mPostalEntries);
    176         mSections.add(mNicknameEntries);
    177         mSections.add(mOrganizationEntries);
    178         mSections.add(mGroupEntries);
    179         mSections.add(mOtherEntries);
    180 
    181         //TODO Read this value from a preference
    182         mShowSmsLinksForAllPhones = true;
    183 
    184         return view;
    185     }
    186 
    187     @Override
    188     public void onInitializeLoaders() {
    189         if (mUri != null) {
    190             loadContact(mUri);
    191         }
    192     }
    193 
    194     @Override
    195     protected Loader onCreateLoader(int id, Bundle args) {
    196         switch (id) {
    197             case LOADER_DETAILS: {
    198                 Uri uri = args.getParcelable(ARG_URI);
    199                 return new ContactLoader(getActivity(), uri);
    200             }
    201         }
    202         return null;
    203     }
    204 
    205     @Override
    206     public void onLoadFinished(Loader<ContactData> loader, ContactData data) {
    207         switch (loader.getId()) {
    208             case LOADER_DETAILS: {
    209                 setData(data);
    210                 break;
    211             }
    212         }
    213     }
    214 
    215     public void loadContact(Uri uri) {
    216         mUri = uri;
    217         Bundle args = new Bundle();
    218         args.putParcelable(ARG_URI, uri);
    219         startLoading(LOADER_DETAILS, args);
    220     }
    221 
    222     public void setData(ContactData data) {
    223         mEntities = data.entities;
    224         mStatuses = data.statuses;
    225 
    226         mNameRawContactId = data.nameRawContactId;
    227         mDisplayNameSource = data.displayNameSource;
    228 
    229         mContactHeaderWidget.bindFromContactLookupUri(data.uri);
    230         bindData();
    231     }
    232 
    233     public interface Controller {
    234         public void onPrimaryAction(ViewEntry entry);
    235         public void onSecondaryAction(ViewEntry entry);
    236     }
    237 
    238     public static final class DefaultController implements Controller {
    239         private Context mContext;
    240 
    241         public DefaultController(Context context) {
    242             mContext = context;
    243         }
    244 
    245         public void onPrimaryAction(ViewEntry entry) {
    246             Intent intent = entry.intent;
    247             if (intent != null) {
    248                 try {
    249                     mContext.startActivity(intent);
    250                 } catch (ActivityNotFoundException e) {
    251                     Log.e(TAG, "No activity found for intent: " + intent);
    252                 }
    253             }
    254         }
    255 
    256         public void onSecondaryAction(ViewEntry entry) {
    257             Intent intent = entry.secondaryIntent;
    258             if (intent != null) {
    259                 try {
    260                     mContext.startActivity(intent);
    261                 } catch (ActivityNotFoundException e) {
    262                     Log.e(TAG, "No activity found for intent: " + intent);
    263                 }
    264             }
    265         }
    266     }
    267 
    268     public void setController(Controller controller) {
    269         mController = controller;
    270     }
    271 
    272     public void onItemClick(AdapterView parent, View v, int position, long id) {
    273         if (mController != null) {
    274             ViewEntry entry = ViewAdapter.getEntry(mSections, position, SHOW_SEPARATORS);
    275             if (entry != null) {
    276                 mController.onPrimaryAction(entry);
    277             }
    278         }
    279     }
    280 
    281     public void onClick(View v) {
    282         if (mController != null) {
    283             mController.onSecondaryAction((ViewEntry) v.getTag());
    284         }
    285     }
    286 
    287     private void bindData() {
    288 
    289         // Build up the contact entries
    290         buildEntries();
    291 
    292         // Collapse similar data items in select sections.
    293         Collapser.collapseList(mPhoneEntries);
    294         Collapser.collapseList(mSmsEntries);
    295         Collapser.collapseList(mEmailEntries);
    296         Collapser.collapseList(mPostalEntries);
    297         Collapser.collapseList(mImEntries);
    298 
    299         if (mAdapter == null) {
    300             mAdapter = new ViewAdapter(getActivity(), mSections);
    301             mListView.setAdapter(mAdapter);
    302         } else {
    303             mAdapter.setSections(mSections, SHOW_SEPARATORS);
    304         }
    305         mListView.setEmptyView(mEmptyView);
    306     }
    307 
    308     /**
    309      * Build up the entries to display on the screen.
    310      *
    311      * @param personCursor the URI for the contact being displayed
    312      */
    313     private final void buildEntries() {
    314         // Clear out the old entries
    315         final int numSections = mSections.size();
    316         for (int i = 0; i < numSections; i++) {
    317             mSections.get(i).clear();
    318         }
    319 
    320         mRawContactIds.clear();
    321 
    322         mReadOnlySourcesCnt = 0;
    323         mWritableSourcesCnt = 0;
    324         mAllRestricted = true;
    325         mPrimaryPhoneUri = null;
    326 
    327         mWritableRawContactIds.clear();
    328 
    329         if (mEntities == null || mStatuses == null) {
    330             return;
    331         }
    332 
    333         final Context context = getActivity();
    334         final Sources sources = Sources.getInstance(context);
    335 
    336         // Build up method entries
    337         for (Entity entity: mEntities) {
    338             final ContentValues entValues = entity.getEntityValues();
    339             final String accountType = entValues.getAsString(RawContacts.ACCOUNT_TYPE);
    340             final long rawContactId = entValues.getAsLong(RawContacts._ID);
    341 
    342             // Mark when this contact has any unrestricted components
    343             final boolean isRestricted = entValues.getAsInteger(RawContacts.IS_RESTRICTED) != 0;
    344             if (!isRestricted) mAllRestricted = false;
    345 
    346             if (!mRawContactIds.contains(rawContactId)) {
    347                 mRawContactIds.add(rawContactId);
    348             }
    349             ContactsSource contactsSource = sources.getInflatedSource(accountType,
    350                     ContactsSource.LEVEL_SUMMARY);
    351             if (contactsSource != null && contactsSource.readOnly) {
    352                 mReadOnlySourcesCnt += 1;
    353             } else {
    354                 mWritableSourcesCnt += 1;
    355                 mWritableRawContactIds.add(rawContactId);
    356             }
    357 
    358 
    359             for (NamedContentValues subValue : entity.getSubValues()) {
    360                 final ContentValues entryValues = subValue.values;
    361                 entryValues.put(Data.RAW_CONTACT_ID, rawContactId);
    362 
    363                 final long dataId = entryValues.getAsLong(Data._ID);
    364                 final String mimeType = entryValues.getAsString(Data.MIMETYPE);
    365                 if (mimeType == null) continue;
    366 
    367                 final DataKind kind = sources.getKindOrFallback(accountType, mimeType,
    368                         context, ContactsSource.LEVEL_MIMETYPES);
    369                 if (kind == null) continue;
    370 
    371                 final ViewEntry entry = ViewEntry.fromValues(context, mimeType, kind,
    372                         rawContactId, dataId, entryValues);
    373 
    374                 final boolean hasData = !TextUtils.isEmpty(entry.data);
    375                 final boolean isSuperPrimary = entryValues.getAsInteger(
    376                         Data.IS_SUPER_PRIMARY) != 0;
    377 
    378                 if (Phone.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    379                     // Build phone entries
    380                     mNumPhoneNumbers++;
    381 
    382                     entry.intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
    383                             Uri.fromParts(Constants.SCHEME_TEL, entry.data, null));
    384                     entry.secondaryIntent = new Intent(Intent.ACTION_SENDTO,
    385                             Uri.fromParts(Constants.SCHEME_SMSTO, entry.data, null));
    386 
    387                     // Remember super-primary phone
    388                     if (isSuperPrimary) mPrimaryPhoneUri = entry.uri;
    389 
    390                     entry.isPrimary = isSuperPrimary;
    391                     mPhoneEntries.add(entry);
    392 
    393                     if (entry.type == CommonDataKinds.Phone.TYPE_MOBILE
    394                             || mShowSmsLinksForAllPhones) {
    395                         // Add an SMS entry
    396                         if (kind.iconAltRes > 0) {
    397                             entry.secondaryActionIcon = kind.iconAltRes;
    398                         }
    399                     }
    400                 } else if (Email.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    401                     // Build email entries
    402                     entry.intent = new Intent(Intent.ACTION_SENDTO,
    403                             Uri.fromParts(Constants.SCHEME_MAILTO, entry.data, null));
    404                     entry.isPrimary = isSuperPrimary;
    405                     mEmailEntries.add(entry);
    406 
    407                     // When Email rows have status, create additional Im row
    408                     final DataStatus status = mStatuses.get(entry.id);
    409                     if (status != null) {
    410                         final String imMime = Im.CONTENT_ITEM_TYPE;
    411                         final DataKind imKind = sources.getKindOrFallback(accountType,
    412                                 imMime, context, ContactsSource.LEVEL_MIMETYPES);
    413                         final ViewEntry imEntry = ViewEntry.fromValues(context,
    414                                 imMime, imKind, rawContactId, dataId, entryValues);
    415                         imEntry.intent = ContactsUtils.buildImIntent(entryValues);
    416                         imEntry.applyStatus(status, false);
    417                         mImEntries.add(imEntry);
    418                     }
    419                 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    420                     // Build postal entries
    421                     entry.maxLines = 4;
    422                     entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
    423                     mPostalEntries.add(entry);
    424                 } else if (Im.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    425                     // Build IM entries
    426                     entry.intent = ContactsUtils.buildImIntent(entryValues);
    427                     if (TextUtils.isEmpty(entry.label)) {
    428                         entry.label = context.getString(R.string.chat).toLowerCase();
    429                     }
    430 
    431                     // Apply presence and status details when available
    432                     final DataStatus status = mStatuses.get(entry.id);
    433                     if (status != null) {
    434                         entry.applyStatus(status, false);
    435                     }
    436                     mImEntries.add(entry);
    437                 } else if (Organization.CONTENT_ITEM_TYPE.equals(mimeType) &&
    438                         (hasData || !TextUtils.isEmpty(entry.label))) {
    439                     // Build organization entries
    440                     final boolean isNameRawContact = (mNameRawContactId == rawContactId);
    441 
    442                     final boolean duplicatesTitle =
    443                         isNameRawContact
    444                         && mDisplayNameSource == DisplayNameSources.ORGANIZATION
    445                         && (!hasData || TextUtils.isEmpty(entry.label));
    446 
    447                     if (!duplicatesTitle) {
    448                         entry.uri = null;
    449 
    450                         if (TextUtils.isEmpty(entry.label)) {
    451                             entry.label = entry.data;
    452                             entry.data = "";
    453                         }
    454 
    455                         mOrganizationEntries.add(entry);
    456                     }
    457                 } else if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    458                     // Build nickname entries
    459                     final boolean isNameRawContact = (mNameRawContactId == rawContactId);
    460 
    461                     final boolean duplicatesTitle =
    462                         isNameRawContact
    463                         && mDisplayNameSource == DisplayNameSources.NICKNAME;
    464 
    465                     if (!duplicatesTitle) {
    466                         entry.uri = null;
    467                         mNicknameEntries.add(entry);
    468                     }
    469                 } else if (Note.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    470                     // Build note entries
    471                     entry.uri = null;
    472                     entry.maxLines = 100;
    473                     mOtherEntries.add(entry);
    474                 } else if (Website.CONTENT_ITEM_TYPE.equals(mimeType) && hasData) {
    475                     // Build note entries
    476                     entry.uri = null;
    477                     entry.maxLines = 10;
    478                     try {
    479                         WebAddress webAddress = new WebAddress(entry.data);
    480                         entry.intent = new Intent(Intent.ACTION_VIEW,
    481                                 Uri.parse(webAddress.toString()));
    482                     } catch (ParseException e) {
    483                         Log.e(TAG, "Couldn't parse website: " + entry.data);
    484                     }
    485                     mOtherEntries.add(entry);
    486                 } else {
    487                     // Handle showing custom rows
    488                     entry.intent = new Intent(Intent.ACTION_VIEW, entry.uri);
    489 
    490                     // Use social summary when requested by external source
    491                     final DataStatus status = mStatuses.get(entry.id);
    492                     final boolean hasSocial = kind.actionBodySocial && status != null;
    493                     if (hasSocial) {
    494                         entry.applyStatus(status, true);
    495                     }
    496 
    497                     if (hasSocial || hasData) {
    498                         mOtherEntries.add(entry);
    499                     }
    500                 }
    501             }
    502         }
    503     }
    504 
    505     static String buildActionString(DataKind kind, ContentValues values, boolean lowerCase,
    506             Context context) {
    507         if (kind.actionHeader == null) {
    508             return null;
    509         }
    510         CharSequence actionHeader = kind.actionHeader.inflateUsing(context, values);
    511         if (actionHeader == null) {
    512             return null;
    513         }
    514         return lowerCase ? actionHeader.toString().toLowerCase() : actionHeader.toString();
    515     }
    516 
    517     static String buildDataString(DataKind kind, ContentValues values, Context context) {
    518         if (kind.actionBody == null) {
    519             return null;
    520         }
    521         CharSequence actionBody = kind.actionBody.inflateUsing(context, values);
    522         return actionBody == null ? null : actionBody.toString();
    523     }
    524 
    525     /**
    526      * A basic structure with the data for a contact entry in the list.
    527      */
    528    public static class ViewEntry extends ContactEntryAdapter.Entry implements Collapsible<ViewEntry> {
    529         public Context context = null;
    530         public String resPackageName = null;
    531         public int actionIcon = -1;
    532         public boolean isPrimary = false;
    533         public int secondaryActionIcon = -1;
    534         public Intent intent;
    535         public Intent secondaryIntent = null;
    536         public int maxLabelLines = 1;
    537         public ArrayList<Long> ids = new ArrayList<Long>();
    538         public int collapseCount = 0;
    539 
    540         public int presence = -1;
    541 
    542         public CharSequence footerLine = null;
    543 
    544         private ViewEntry() {
    545         }
    546 
    547         /**
    548          * Build new {@link ViewEntry} and populate from the given values.
    549          */
    550         public static ViewEntry fromValues(Context context, String mimeType, DataKind kind,
    551                 long rawContactId, long dataId, ContentValues values) {
    552             final ViewEntry entry = new ViewEntry();
    553             entry.context = context;
    554             entry.contactId = rawContactId;
    555             entry.id = dataId;
    556             entry.uri = ContentUris.withAppendedId(Data.CONTENT_URI, entry.id);
    557             entry.mimetype = mimeType;
    558             entry.label = buildActionString(kind, values, false, context);
    559             entry.data = buildDataString(kind, values, context);
    560 
    561             if (kind.typeColumn != null && values.containsKey(kind.typeColumn)) {
    562                 entry.type = values.getAsInteger(kind.typeColumn);
    563             }
    564             if (kind.iconRes > 0) {
    565                 entry.resPackageName = kind.resPackageName;
    566                 entry.actionIcon = kind.iconRes;
    567             }
    568 
    569             return entry;
    570         }
    571 
    572         /**
    573          * Apply given {@link DataStatus} values over this {@link ViewEntry}
    574          *
    575          * @param fillData When true, the given status replaces {@link #data}
    576          *            and {@link #footerLine}. Otherwise only {@link #presence}
    577          *            is updated.
    578          */
    579         public ViewEntry applyStatus(DataStatus status, boolean fillData) {
    580             presence = status.getPresence();
    581             if (fillData && status.isValid()) {
    582                 this.data = status.getStatus().toString();
    583                 this.footerLine = status.getTimestampLabel(context);
    584             }
    585 
    586             return this;
    587         }
    588 
    589         public boolean collapseWith(ViewEntry entry) {
    590             // assert equal collapse keys
    591             if (!shouldCollapseWith(entry)) {
    592                 return false;
    593             }
    594 
    595             // Choose the label associated with the highest type precedence.
    596             if (TypePrecedence.getTypePrecedence(mimetype, type)
    597                     > TypePrecedence.getTypePrecedence(entry.mimetype, entry.type)) {
    598                 type = entry.type;
    599                 label = entry.label;
    600             }
    601 
    602             // Choose the max of the maxLines and maxLabelLines values.
    603             maxLines = Math.max(maxLines, entry.maxLines);
    604             maxLabelLines = Math.max(maxLabelLines, entry.maxLabelLines);
    605 
    606             // Choose the presence with the highest precedence.
    607             if (StatusUpdates.getPresencePrecedence(presence)
    608                     < StatusUpdates.getPresencePrecedence(entry.presence)) {
    609                 presence = entry.presence;
    610             }
    611 
    612             // If any of the collapsed entries are primary make the whole thing primary.
    613             isPrimary = entry.isPrimary ? true : isPrimary;
    614 
    615             // uri, and contactdId, shouldn't make a difference. Just keep the original.
    616 
    617             // Keep track of all the ids that have been collapsed with this one.
    618             ids.add(entry.id);
    619             collapseCount++;
    620             return true;
    621         }
    622 
    623         public boolean shouldCollapseWith(ViewEntry entry) {
    624             if (entry == null) {
    625                 return false;
    626             }
    627 
    628             if (!ContactsUtils.areDataEqual(context, mimetype, data, entry.mimetype, entry.data)) {
    629                 return false;
    630             }
    631 
    632             if (!TextUtils.equals(mimetype, entry.mimetype)
    633                     || !ContactsUtils.areIntentActionEqual(intent, entry.intent)
    634                     || !ContactsUtils.areIntentActionEqual(secondaryIntent, entry.secondaryIntent)
    635                     || actionIcon != entry.actionIcon) {
    636                 return false;
    637             }
    638 
    639             return true;
    640         }
    641     }
    642 
    643     /** Cache of the children views of a row */
    644     static class ViewCache {
    645         public TextView label;
    646         public TextView data;
    647         public TextView footer;
    648         public ImageView actionIcon;
    649         public ImageView presenceIcon;
    650         public ImageView primaryIcon;
    651         public ImageView secondaryActionButton;
    652         public View secondaryActionDivider;
    653 
    654         // Need to keep track of this too
    655         ViewEntry entry;
    656     }
    657 
    658     private final class ViewAdapter extends ContactEntryAdapter<ViewEntry> {
    659         ViewAdapter(Context context, ArrayList<ArrayList<ViewEntry>> sections) {
    660             super(context, sections, SHOW_SEPARATORS);
    661         }
    662 
    663         @Override
    664         public View getView(int position, View convertView, ViewGroup parent) {
    665             ViewEntry entry = getEntry(mSections, position, false);
    666             View v;
    667 
    668             ViewCache views;
    669 
    670             // Check to see if we can reuse convertView
    671             if (convertView != null) {
    672                 v = convertView;
    673                 views = (ViewCache) v.getTag();
    674             } else {
    675                 // Create a new view if needed
    676                 v = mInflater.inflate(R.layout.list_item_text_icons, parent, false);
    677 
    678                 // Cache the children
    679                 views = new ViewCache();
    680                 views.label = (TextView) v.findViewById(android.R.id.text1);
    681                 views.data = (TextView) v.findViewById(android.R.id.text2);
    682                 views.footer = (TextView) v.findViewById(R.id.footer);
    683                 views.actionIcon = (ImageView) v.findViewById(R.id.action_icon);
    684                 views.primaryIcon = (ImageView) v.findViewById(R.id.primary_icon);
    685                 views.presenceIcon = (ImageView) v.findViewById(R.id.presence_icon);
    686                 views.secondaryActionButton = (ImageView) v.findViewById(
    687                         R.id.secondary_action_button);
    688                 views.secondaryActionButton.setOnClickListener(ContactFragment.this);
    689                 views.secondaryActionDivider = v.findViewById(R.id.divider);
    690                 v.setTag(views);
    691             }
    692 
    693             // Update the entry in the view cache
    694             views.entry = entry;
    695 
    696             // Bind the data to the view
    697             bindView(v, entry);
    698             return v;
    699         }
    700 
    701         @Override
    702         protected View newView(int position, ViewGroup parent) {
    703             // getView() handles this
    704             throw new UnsupportedOperationException();
    705         }
    706 
    707         @Override
    708         protected void bindView(View view, ViewEntry entry) {
    709             final Resources resources = mContext.getResources();
    710             ViewCache views = (ViewCache) view.getTag();
    711 
    712             // Set the label
    713             TextView label = views.label;
    714             setMaxLines(label, entry.maxLabelLines);
    715             label.setText(entry.label);
    716 
    717             // Set the data
    718             TextView data = views.data;
    719             if (data != null) {
    720                 if (entry.mimetype.equals(Phone.CONTENT_ITEM_TYPE)
    721                         || entry.mimetype.equals(Constants.MIME_SMS_ADDRESS)) {
    722                     data.setText(PhoneNumberUtils.formatNumber(entry.data));
    723                 } else {
    724                     data.setText(entry.data);
    725                 }
    726                 setMaxLines(data, entry.maxLines);
    727             }
    728 
    729             // Set the footer
    730             if (!TextUtils.isEmpty(entry.footerLine)) {
    731                 views.footer.setText(entry.footerLine);
    732                 views.footer.setVisibility(View.VISIBLE);
    733             } else {
    734                 views.footer.setVisibility(View.GONE);
    735             }
    736 
    737             // Set the primary icon
    738             views.primaryIcon.setVisibility(entry.isPrimary ? View.VISIBLE : View.GONE);
    739 
    740             // Set the action icon
    741             ImageView action = views.actionIcon;
    742             if (entry.actionIcon != -1) {
    743                 Drawable actionIcon;
    744                 if (entry.resPackageName != null) {
    745                     // Load external resources through PackageManager
    746                     actionIcon = mContext.getPackageManager().getDrawable(entry.resPackageName,
    747                             entry.actionIcon, null);
    748                 } else {
    749                     actionIcon = resources.getDrawable(entry.actionIcon);
    750                 }
    751                 action.setImageDrawable(actionIcon);
    752                 action.setVisibility(View.VISIBLE);
    753             } else {
    754                 // Things should still line up as if there was an icon, so make it invisible
    755                 action.setVisibility(View.INVISIBLE);
    756             }
    757 
    758             // Set the presence icon
    759             Drawable presenceIcon = ContactPresenceIconUtil.getPresenceIcon(
    760                     mContext, entry.presence);
    761             ImageView presenceIconView = views.presenceIcon;
    762             if (presenceIcon != null) {
    763                 presenceIconView.setImageDrawable(presenceIcon);
    764                 presenceIconView.setVisibility(View.VISIBLE);
    765             } else {
    766                 presenceIconView.setVisibility(View.GONE);
    767             }
    768 
    769             // Set the secondary action button
    770             ImageView secondaryActionView = views.secondaryActionButton;
    771             Drawable secondaryActionIcon = null;
    772             if (entry.secondaryActionIcon != -1) {
    773                 secondaryActionIcon = resources.getDrawable(entry.secondaryActionIcon);
    774             }
    775             if (entry.secondaryIntent != null && secondaryActionIcon != null) {
    776                 secondaryActionView.setImageDrawable(secondaryActionIcon);
    777                 secondaryActionView.setTag(entry);
    778                 secondaryActionView.setVisibility(View.VISIBLE);
    779                 views.secondaryActionDivider.setVisibility(View.VISIBLE);
    780             } else {
    781                 secondaryActionView.setVisibility(View.GONE);
    782                 views.secondaryActionDivider.setVisibility(View.GONE);
    783             }
    784         }
    785 
    786         private void setMaxLines(TextView textView, int maxLines) {
    787             if (maxLines == 1) {
    788                 textView.setSingleLine(true);
    789                 textView.setEllipsize(TextUtils.TruncateAt.END);
    790             } else {
    791                 textView.setSingleLine(false);
    792                 textView.setMaxLines(maxLines);
    793                 textView.setEllipsize(null);
    794             }
    795         }
    796     }
    797 }
    798